ios-swiftui-patterns
Use when building SwiftUI views, managing state with @State/@Binding/@ObservableObject, or implementing declarative UI patterns in iOS apps.
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
iOS - SwiftUI Patterns
Modern declarative UI development for iOS, macOS, watchOS, and tvOS applications.
Key Concepts
State Management Hierarchy
SwiftUI provides a hierarchy of property wrappers for different state needs:
- @State: Local view state, owned by the view
- @Binding: Two-way connection to state owned elsewhere
- @StateObject: Creates and owns an ObservableObject
- @ObservedObject: References an ObservableObject owned elsewhere
- @EnvironmentObject: Dependency injection through the view hierarchy
- @Environment: Access to system-provided values
Observable Pattern (iOS 17+)
@Observable
class UserModel {
var name: String = ""
var email: String = ""
var isLoggedIn: Bool = false
}
struct ContentView: View {
@State private var user = UserModel()
var body: some View {
UserProfileView(user: user)
}
}
Legacy ObservableObject Pattern
class UserViewModel: ObservableObject {
@Published var name: String = ""
@Published var isLoading: Bool = false
func fetchUser() async {
isLoading = true
defer { isLoading = false }
// fetch logic
}
}
struct UserView: View {
@StateObject private var viewModel = UserViewModel()
var body: some View {
// view implementation
}
}
Best Practices
View Composition
Break complex views into smaller, focused components:
struct OrderSummaryView: View {
let order: Order
var body: some View {
VStack(spacing: 16) {
OrderHeaderView(order: order)
OrderItemsListView(items: order.items)
OrderTotalView(total: order.total)
}
}
}
Prefer Value Types
Use structs for models when possible to leverage SwiftUI's efficient diffing:
struct Product: Identifiable, Equatable {
let id: UUID
var name: String
var price: Decimal
var quantity: Int
}
Use ViewModifiers for Reusable Styling
struct CardModifier: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(radius: 4)
}
}
extension View {
func cardStyle() -> some View {
modifier(CardModifier())
}
}
Task Lifecycle for Async Work
struct UserDetailView: View {
let userId: String
@State private var user: User?
var body: some View {
Group {
if let user {
UserContent(user: user)
} else {
ProgressView()
}
}
.task {
user = await fetchUser(id: userId)
}
}
}
Common Patterns
Navigation with NavigationStack (iOS 16+)
struct ContentView: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
ProductListView()
.navigationDestination(for: Product.self) { product in
ProductDetailView(product: product)
}
.navigationDestination(for: Category.self) { category in
CategoryView(category: category)
}
}
}
}
Sheet and Alert Presentation
struct ItemView: View {
@State private var showingDetail = false
@State private var showingDeleteAlert = false
var body: some View {
Button("View Details") {
showingDetail = true
}
.sheet(isPresented: $showingDetail) {
DetailSheet()
}
.alert("Delete Item?", isPresented: $showingDeleteAlert) {
Button("Delete", role: .destructive) { deleteItem() }
Button("Cancel", role: .cancel) { }
}
}
}
List with SwiftData (iOS 17+)
@Model
class Task {
var title: String
var isCompleted: Bool
var createdAt: Date
init(title: String) {
self.title = title
self.isCompleted = false
self.createdAt = Date()
}
}
struct TaskListView: View {
@Query(sort: \Task.createdAt, order: .reverse)
private var tasks: [Task]
@Environment(\.modelContext) private var modelContext
var body: some View {
List(tasks) { task in
TaskRowView(task: task)
}
}
}
Anti-Patterns
Avoid Large Monolithic Views
Bad:
struct BadView: View {
var body: some View {
VStack {
// 200+ lines of nested views
}
}
}
Good: Extract into focused subviews.
Don't Use @ObservedObject for Owned State
Bad:
struct BadView: View {
@ObservedObject var viewModel = ViewModel() // Re-created on every view init!
}
Good:
struct GoodView: View {
@StateObject private var viewModel = ViewModel()
}
Avoid Side Effects in View Body
Bad:
var body: some View {
let _ = print("View rendered") // Side effect!
Text("Hello")
}
Good: Use .task, .onAppear, or .onChange for side effects.
Don't Force Unwrap in Views
Bad:
Text(user!.name) // Crash risk
Good:
if let user {
Text(user.name)
}
Related Skills
- ios-swift-concurrency: Async/await patterns for data loading
- ios-uikit-architecture: When bridging UIKit and SwiftUI
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-01-20
- Index updated
- 2026-02-04
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!