Best practices and patterns for building SwiftUI views and components. Use when creating or refactoring SwiftUI UI, designing tab architecture, composing screens, or needing component-specific guidance.
From ios-swift-skillsnpx claudepluginhub patrickserrano/skillsThis skill uses the workspace's default tool permissions.
rg "TabView\(" or similarAppTab enum and RouterPath@State, @Binding, @Observable, @Environment).task and explicit loading/error states.sheet(item:) over .sheet(isPresented:) when state represents a selected modelif let inside a sheet bodydismiss() internally@Environment.task and explicit state enum if neededSwiftUI views should be lightweight state expressions. Avoid ViewModels unless truly necessary.
struct FeedView: View {
@Environment(APIClient.self) private var client
enum ViewState {
case loading
case error(String)
case loaded([Post])
}
@State private var viewState: ViewState = .loading
var body: some View {
NavigationStack {
List {
switch viewState {
case .loading:
ProgressView("Loading...")
case .error(let message):
ErrorView(message: message, retry: { await loadFeed() })
case .loaded(let posts):
ForEach(posts) { post in
PostRow(post: post)
}
}
}
.task { await loadFeed() }
}
}
private func loadFeed() async {
do {
let posts = try await client.getFeed()
viewState = .loaded(posts)
} catch {
viewState = .error(error.localizedDescription)
}
}
}
@State private var selectedItem: Item?
.sheet(item: $selectedItem) { item in
EditItemSheet(item: item)
}
struct EditItemSheet: View {
@Environment(\.dismiss) private var dismiss
@Environment(Store.self) private var store
let item: Item
@State private var isSaving = false
var body: some View {
VStack {
Button(isSaving ? "Saving..." : "Save") {
Task { await save() }
}
}
}
private func save() async {
isSaving = true
await store.save(item)
dismiss()
}
}
@main
struct MyApp: App {
@State var client: APIClient = .init()
@State var router: AppRouter = .init()
var body: some Scene {
WindowGroup {
TabView(selection: $router.selectedTab) {
ForEach(AppTab.allCases) { tab in
tab.rootView
.tag(tab)
}
}
.environment(client)
.environment(router)
}
}
}
| Wrapper | Use Case |
|---|---|
@State | Local, ephemeral view state |
@Binding | Two-way data flow from parent |
@Observable | Shared state across views (iOS 17+) |
@Environment | Dependency injection, app-wide concerns |
@Query | SwiftData queries directly in views |
// React to state changes
.task(id: searchText) {
guard !searchText.isEmpty else { return }
await search(query: searchText)
}
// Respond to state transitions
.onChange(of: isActive, initial: false) {
guard isActive else { return }
Task { await refresh() }
}
SwiftUI was designed without ViewModels in mind:
@State, @Environment, @Observable handle all data flow needs@Query works directly in viewsInstead:
@Environment