From ecc
SwiftUI 아키텍처 패턴, @Observable을 사용한 상태 관리, 뷰 구성, 네비게이션, 성능 최적화 및 현대적인 iOS/macOS UI 모범 사례입니다.
npx claudepluginhub sam42-lab/everything-claude-code-krThis skill uses the workspace's default tool permissions.
Apple 플랫폼에서 선언적이고 성능이 뛰어난 사용자 인터페이스를 구축하기 위한 현대적인 SwiftUI 패턴입니다. Observation 프레임워크, 뷰 구성, 타입 안전 네비게이션 및 성능 최적화를 다룹니다.
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
Apple 플랫폼에서 선언적이고 성능이 뛰어난 사용자 인터페이스를 구축하기 위한 현대적인 SwiftUI 패턴입니다. Observation 프레임워크, 뷰 구성, 타입 안전 네비게이션 및 성능 최적화를 다룹니다.
@State, @Observable, @Binding)NavigationStack을 사용하여 네비게이션 흐름을 설계할 때상황에 맞는 가장 단순한 래퍼를 선택하세요:
| 래퍼 | 사용 사례 |
|---|---|
@State | 뷰 로컬 값 타입 (토글, 폼 필드, 시트 표시 여부 등) |
@Binding | 부모 뷰의 @State에 대한 양방향 참조 |
@Observable 클래스 + @State | 여러 프로퍼티를 가진 소유된 모델 |
@Observable 클래스 (래퍼 없음) | 부모로부터 전달받은 읽기 전용 참조 |
@Bindable | @Observable 프로퍼티에 대한 양방향 바인딩 |
@Environment | .environment()를 통해 주입된 공유 의존성 |
ObservableObject 대신 @Observable을 사용하세요. 프로퍼티 수준의 변경을 추적하므로, 변경된 프로퍼티를 읽는 뷰만 다시 렌더링됩니다.
@Observable
final class ItemListViewModel {
private(set) var items: [Item] = []
private(set) var isLoading = false
var searchText = ""
private let repository: any ItemRepository
init(repository: any ItemRepository = DefaultItemRepository()) {
self.repository = repository
}
func load() async {
isLoading = true
defer { isLoading = false }
items = (try? await repository.fetchAll()) ?? []
}
}
struct ItemListView: View {
@State private var viewModel: ItemListViewModel
init(viewModel: ItemListViewModel = ItemListViewModel()) {
_viewModel = State(initialValue: viewModel)
}
var body: some View {
List(viewModel.items) { item in
ItemRow(item = item)
}
.searchable(text: $viewModel.searchText)
.overlay { if viewModel.isLoading { ProgressView() } }
.task { await viewModel.load() }
}
}
@EnvironmentObject를 @Environment로 교체하세요:
// 주입
ContentView()
.environment(authManager)
// 사용
struct ProfileView: View {
@Environment(AuthManager.self) private var auth
var body: some View {
Text(auth.currentUser?.name ?? "Guest")
}
}
뷰를 작고 집중된 구조체로 분리하세요. 상태가 변경될 때 해당 상태를 읽는 하위 뷰만 다시 렌더링됩니다.
struct OrderView: View {
@State private var viewModel = OrderViewModel()
var body: some View {
VStack {
OrderHeader(title: viewModel.title)
OrderItemList(items: viewModel.items)
OrderTotal(total: viewModel.total)
}
}
}
struct CardModifier: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
extension View {
func cardStyle() -> some View {
modifier(CardModifier())
}
}
프로그래밍 방식의 타입 안전 라우팅을 위해 NavigationStack과 NavigationPath를 사용하세요.
@Observable
final class Router {
var path = NavigationPath()
func navigate(to destination: Destination) {
path.append(destination)
}
func popToRoot() {
path = NavigationPath()
}
}
enum Destination: Hashable {
case detail(Item.ID)
case settings
case profile(User.ID)
}
struct RootView: View {
@State private var router = Router()
var body: some View {
NavigationStack(path: $router.path) {
HomeView()
.navigationDestination(for: Destination.self) { dest in
switch dest {
case .detail(let id): ItemDetailView(itemID: id)
case .settings: SettingsView()
case .profile(let id): ProfileView(userID: id)
}
}
}
.environment(router)
}
}
LazyVStack과 LazyHStack은 화면에 보일 때만 뷰를 생성합니다.
ScrollView {
LazyVStack(spacing: 8) {
ForEach(items) { item in
ItemRow(item: item)
}
}
}
ForEach에서는 항상 안정적이고 고유한 ID를 사용하세요. 배열 인덱스 사용을 피하세요.
// Identifiable 준수 또는 명시적 id 사용
ForEach(items, id: \.stableID) { item in
ItemRow(item: item)
}
body 내부에서 I/O, 네트워크 호출 또는 무거운 계산을 수행하지 마세요..task {}를 사용하세요. 뷰가 사라질 때 자동으로 취소됩니다..sensoryFeedback() 및 .geometryGroup() 사용을 절제하세요..shadow(), .blur(), .mask() 사용을 최소화하세요. 오프스크린 렌더링을 유발합니다.body 연산이 매우 무거운 뷰의 경우, Equatable을 준수하여 불필요한 재렌더링을 건너뛰게 하세요.
struct ExpensiveChartView: View, Equatable {
let dataPoints: [DataPoint] // DataPoint는 Equatable을 준수해야 함
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.dataPoints == rhs.dataPoints
}
var body: some View {
// 복잡한 차트 렌더링 로직
}
}
빠른 반복 개발을 위해 인라인 모킹 데이터를 포함한 #Preview 매크로를 사용하세요.
#Preview("비어 있는 상태") {
ItemListView(viewModel: ItemListViewModel(repository: EmptyMockRepository()))
}
#Preview("데이터 있음") {
ItemListView(viewModel: ItemListViewModel(repository: PopulatedMockRepository()))
}
ObservableObject / @Published / @StateObject / @EnvironmentObject 사용 — @Observable로 마이그레이션하세요.body나 init에서 직접 비동기 작업 수행 — .task {}나 명시적인 로드 메서드를 사용하세요.@State로 생성 — 부모로부터 전달받으세요.AnyView 타입 지우기 사용 — 조건부 뷰에는 @ViewBuilder나 Group을 선호하세요.Sendable 요구 사항 무시.액터 기반 영속성 패턴은 swift-actor-persistence 스킬을 참조하세요.
프로토콜 기반 DI 및 Swift Testing을 사용한 테스트는 swift-protocol-di-testing 스킬을 참조하세요.