SwiftUI Patterns · v1.0.0 · 2026-03-29 · sha256 0ee56a567f8efce5
SwiftUI Patterns v1.0.0A
Immutable. This exact content is served forever at /api/v1/blob/0ee56a567f8efce5.
---
name: SwiftUI Patterns
description: Modern SwiftUI patterns for iOS 18+, including @Observable, navigation, state management, and view composition following SOLID principles
version: 1.0.0
---
# SwiftUI Patterns Skill
Master modern SwiftUI development with Swift 6 features, @Observable macro, NavigationStack, and architectural patterns that follow SOLID principles.
## State Management (iOS 18+ / Swift 6)
### @Observable Macro (Preferred over ObservableObject)
The `@Observable` macro is the modern way to create observable state in SwiftUI:
```swift
import SwiftUI
@Observable
final class UserViewModel {
var user: User?
var isLoading = false
var errorMessage: String?
private let repository: UserRepositoryProtocol
init(repository: UserRepositoryProtocol) {
self.repository = repository
}
@MainActor
func loadUser(id: String) async {
isLoading = true
defer { isLoading = false }
do {
user = try await repository.fetch(id: id)
} catch {
errorMessage = error.localizedDescription
}
}
}
```
### State Property Wrappers
| Wrapper | Use Case | Scope |
|---------|----------|-------|
| `@State` | View-local primitive state | Single view |
| `@Binding` | Two-way connection to parent state | Parent-child |
| `@Bindable` | Two-way binding to @Observable property | With @Observable |
| `@Environment` | Dependency injection | Environment chain |
| `@Observable` | Shared mutable state (replaces @ObservableObject) | Across views |
### Using @Bindable with @Observable
```swift
@Observable
final class FormViewModel {
var email = ""
var password = ""
var isValid: Bool {
!email.isEmpty && password.count >= 8
}
}
struct LoginForm: View {
@Bindable var viewModel: FormViewModel
var body: some View {
Form {
TextField("Email", text: $viewModel.email)
SecureField("Password", text: $viewModel.password)
Button("Login") { /* ... */ }
.disabled(!viewModel.isValid)
}
}
}
```
## Navigation (NavigationStack)
### Type-Safe Navigation with Enums
```swift
enum AppRoute: Hashable {
case userProfile(User.ID)
case settings
case orderDetail(Order)
case checkout(Cart)
}
struct ContentView: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
HomeView(path: $path)
.navigationDestination(for: AppRoute.self) { route in
destinationView(for: route)
}
}
}
@ViewBuilder
private func destinationView(for route: AppRoute) -> some View {
switch route {
case .userProfile(let userId):
UserProfileView(userId: userId)
case .settings:
SettingsView()
case .orderDetail(let order):
OrderDetailView(order: order)
case .checkout(let cart):
CheckoutView(cart: cart)
}
}
}
```
### Programmatic Navigation
```swift
struct ProductListView: View {
@Binding var path: NavigationPath
let products: [Product]
var body: some View {
List(products) { product in
Button {
path.append(AppRoute.productDetail(product.id))
} label: {
ProductRow(product: product)
}
}
}
func navigateToCheckout(cart: Cart) {
path.append(AppRoute.checkout(cart))
}
func popToRoot() {
path.removeLast(path.count)
}
}
```
## View Composition
### Extract Subviews (Single Responsibility)
```swift
// BAD: Monolithic view
struct OrderView: View {
let order: Order
var body: some View {
VStack {
// 100+ lines of UI code
}
}
}
// GOOD: Composed from smaller views
struct OrderView: View {
let order: Order
var body: some View {
ScrollView {
VStack(spacing: 16) {
OrderHeaderView(order: order)
OrderItemsSection(items: order.items)
OrderSummaryView(
subtotal: order.subtotal,
tax: order.tax,
total: order.total
)
OrderActionsView(order: order)
}
.padding()
}
}
}
```
### Reusable Components with ViewBuilder
```swift
struct Card<Content: View>: View {
let title: String?
@ViewBuilder let content: () -> Content
init(title: String? = nil, @ViewBuilder content: @escaping () -> Content) {
self.title = title
self.content = content
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
if let title {
Text(title)
.font(.headline)
}
content()
}
.padding()
.background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
// Usage
Card(title: "Order Summary") {
LabeledContent("Subtotal", value: order.subtotal.formatted(.currency(code: "USD")))
LabeledContent("Tax", value: order.tax.formatted(.currency(code: "USD")))
Divider()
LabeledContent("Total", value: order.total.formatted(.currency(code: "USD")))
.bold()
}
```
### ViewModifiers for Reusable Styling
```swift
struct CardModifier: ViewModifier {
let cornerRadius: CGFloat
func body(content: Content) -> some View {
content
.padding()
.background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: cornerRadius))
.shadow(radius: 2)
}
}
extension View {
func cardStyle(cornerRadius: CGFloat = 12) -> some View {
modifier(CardModifier(cornerRadius: cornerRadius))
}
}
// Usage
Text("Hello")
.cardStyle()
```
## SOLID Principles in SwiftUI
### Single Responsibility Principle
Each view should have one reason to change:
```swift
// View: Only presentation
struct UserListView: View {
@State private var viewModel: UserListViewModel
var body: some View {
List(viewModel.users) { user in
UserRow(user: user)
}
.task { await viewModel.loadUsers() }
}
}
// ViewModel: Business logic and state
@Observable @MainActor
final class UserListViewModel {
private let service: UserServiceProtocol
var users: [User] = []
init(service: UserServiceProtocol) {
self.service = service
}
func loadUsers() async {
users = (try? await service.fetchAll()) ?? []
}
}
```
### Open/Closed Principle
Extend behavior without modifying existing code:
```swift
// Base button style
struct PrimaryButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding()
.background(Color.accentColor)
.foregroundStyle(.white)
.clipShape(RoundedRectangle(cornerRadius: 8))
.opacity(configuration.isPressed ? 0.8 : 1)
}
}
// Extended without modifying original
struct LoadingButtonStyle: ButtonStyle {
let isLoading: Bool
func makeBody(configuration: Configuration) -> some View {
HStack {
if isLoading {
ProgressView()
.tint(.white)
}
configuration.label
}
.padding()
.background(Color.accentColor)
.foregroundStyle(.white)
.clipShape(RoundedRectangle(cornerRadius: 8))
.opacity(configuration.isPressed || isLoading ? 0.8 : 1)
}
}
```
### Dependency Inversion Principle
Depend on abstractions via Environment:
```swift
// Protocol abstraction
protocol AnalyticsProtocol {
func track(event: String, properties: [String: Any])
}
// Environment key
private struct AnalyticsKey: EnvironmentKey {
static let defaultValue: AnalyticsProtocol = NoOpAnalytics()
}
extension EnvironmentValues {
var analytics: AnalyticsProtocol {
get { self[AnalyticsKey.self] }
set { self[AnalyticsKey.self] = newValue }
}
}
// Usage in view
struct ProductView: View {
@Environment(\.analytics) private var analytics
let product: Product
var body: some View {
Button("Buy") {
analytics.track(event: "purchase", properties: ["product_id": product.id])
}
}
}
// Inject real or mock
ProductView(product: product)
.environment(\.analytics, FirebaseAnalytics()) // Production
.environment(\.analytics, MockAnalytics()) // Testing
```
## Performance Optimization
### Avoid Expensive Body Computations
```swift
// BAD: Expensive computation in body
var body: some View {
let sortedItems = items.sorted { $0.date > $1.date } // Called on every render
List(sortedItems) { item in
ItemRow(item: item)
}
}
// GOOD: Computed outside body or cached
@Observable
final class ItemListViewModel {
var items: [Item] = []
var sortedItems: [Item] {
items.sorted { $0.date > $1.date }
}
}
```
### Use `@Previewable` for Previews (iOS 18+)
```swift
#Preview {
@Previewable @State var count = 0
VStack {
Text("Count: \(count)")
Button("+1") { count += 1 }
}
}
```
### Lazy Loading
```swift
LazyVStack {
ForEach(items) { item in
ItemRow(item: item)
}
}
LazyVGrid(columns: [GridItem(.adaptive(minimum: 150))]) {
ForEach(products) { product in
ProductCard(product: product)
}
}
```
## References
- See `references/observable-migration.md` for ObservableObject to @Observable migration guide
- See `references/navigation-patterns.md` for advanced navigation patterns
- See `references/mvvm-architecture.md` for complete MVVM implementation