Apple Development Best Practices
Modern Apple platform development using Swift 6 and SwiftUI as primary frameworks.
When to Use
Use this skill when:
- Building iOS or macOS apps with Swift 6 and SwiftUI
- Designing navigation, state management, or concurrency patterns for Apple platforms
- Working with SwiftData, Core Data, StoreKit, CloudKit, or other Apple frameworks
- Reviewing Swift code or planning Apple platform architecture
When NOT to Use
Do NOT use this skill when:
- Building cross-platform mobile apps (Flutter, React Native, Kotlin Multiplatform) — use a cross-platform mobile persona instead, because Apple-specific patterns like
@Observable and NavigationStack don't apply
- Writing server-side Swift (Vapor, Hummingbird) — use a backend engineering persona instead, because server-side Swift has different concurrency, deployment, and architecture concerns
Core Philosophy
Build apps that are previewable, testable, and maintainable. A previewable app is a testable app. A testable app is a maintainable app.
Swift 6 Standards
- Strict concurrency enabled — treat all warnings as errors
@Observable over ObservableObject (iOS 17+)
async/await for all asynchronous operations
- Value types (structs) preferred over reference types (classes) unless identity semantics needed
guard for early exits, never deeply nested if let chains
- Typed errors via
LocalizedError conformance — no raw strings
- No force unwrapping (
!) without documented justification
- Follow Apple's Swift API Design Guidelines for naming
SwiftUI Architecture
State Management — Single Source of Truth (SSOT)
// Local view state → @State
@State private var isExpanded = false
// Observable model → @State with @Observable class
@State private var viewModel = RecipeViewModel()
// Shared across view tree → @Environment
@Environment(\.recipeStore) private var store
// Bindings to @Observable → @Bindable
@Bindable var viewModel: RecipeViewModel
Rules:
@State for view-local state only — never shared across views
@Observable classes for ViewModels (replaces ObservableObject + @Published)
@Environment for dependency injection (services, stores, settings)
- Never pass view models more than 2 levels deep — use Environment instead
Navigation — NavigationStack with Type-Safe Routing
enum Route: Hashable {
case recipeDetail(Recipe)
case settings
case profile(User)
}
@Observable
final class Router {
var path = NavigationPath()
func navigate(to route: Route) {
path.append(route)
}
}
Rules:
NavigationStack only — never deprecated NavigationView
- Type-safe routing via
Hashable enum
- Router as
@Observable class in @Environment
- Sheet presentation via optional ViewModel on parent
View Composition
- Extract subviews at 50+ lines or when reusable
- Max 100 lines per view file before mandatory extraction
- Custom ViewModifiers for shared styling — not repeated inline styles
- Never use
AnyView — destroys diffing performance and identity
- Prefer
@ViewBuilder closures over AnyView for type erasure
Performance
LazyVStack/LazyHStack inside ScrollView — never eager stacks for large lists
EquatableView wrapper for complex views that rarely change
- Keep view body pure — no side effects, no network calls
- Use
.task modifier for async work, not onAppear with Task
- Profile with SwiftUI Performance Instrument (Xcode 16+)
Project Structure
AppName/
├── App/ # App entry, lifecycle, configuration
│ ├── AppNameApp.swift
│ └── AppDelegate.swift # Only if needed for UIKit integration
├── Features/ # Feature modules (self-contained)
│ ├── Recipes/
│ │ ├── Views/ # SwiftUI views
│ │ ├── ViewModels/ # @Observable classes
│ │ └── Models/ # Data models (structs)
│ ├── MealPlanning/
│ └── Community/
├── Core/ # Shared infrastructure
│ ├── Extensions/
│ ├── Services/ # Networking, auth, analytics
│ ├── Persistence/ # SwiftData / Core Data
│ └── Components/ # Reusable UI components
├── Resources/ # Assets, Localizations, Fonts
└── Tests/
├── UnitTests/ # ViewModel + Service tests
└── UITests/ # Critical user flow tests
Rules:
- Features are self-contained — no cross-feature imports
- Shared code lives in
Core/ only
- Each feature has its own Views, ViewModels, Models
- Feature folders mirror navigation structure
Concurrency
// Actor for thread-safe shared state
actor RecipeStore {
private var cache: [UUID: Recipe] = [:]
func recipe(for id: UUID) -> Recipe? {
cache[id]
}
}
// @MainActor for UI-bound classes
@MainActor
@Observable
final class RecipeListViewModel {
var recipes: [Recipe] = []
var isLoading = false
func loadRecipes() async {
isLoading = true
defer { isLoading = false }
recipes = await recipeService.fetchAll()
}
}
Rules:
@MainActor on all ViewModels
actor for shared mutable state
Sendable conformance for types crossing isolation boundaries
- Never
DispatchQueue.main.async — use @MainActor instead
Task only inside .task modifier or explicit user-initiated actions
TaskGroup for parallel independent work
Testing
- Swift Testing (
@Test, #expect) preferred over XCTest for new code
- Unit tests for all ViewModel logic — 80%+ coverage on business logic
- UI tests for critical user flows only (login, purchase, core CRUD)
- Dependency injection via protocols for testability
- No singletons in production code — inject via
@Environment
- Preview-driven development: if a view is hard to preview, it's hard to test
Persistence
SwiftData (iOS 17+) is the default persistence layer:
@Model
final class Recipe {
var name: String
var ingredients: [String]
var instructions: String
@Relationship(deleteRule: .cascade)
var steps: [CookingStep]
}
Rules:
@Model classes for SwiftData — not structs
- Define
@Relationship explicitly with delete rules
- Use
@Query in views for automatic updates
- ModelContainer configured in App entry point
- Migration strategy documented before schema changes
Networking
protocol RecipeServiceProtocol: Sendable {
func fetchAll() async throws -> [Recipe]
func create(_ recipe: Recipe) async throws -> Recipe
}
struct RecipeService: RecipeServiceProtocol {
private let session: URLSession
private let decoder: JSONDecoder
func fetchAll() async throws -> [Recipe] {
let (data, response) = try await session.data(from: endpoint)
guard let http = response as? HTTPURLResponse,
(200...299).contains(http.statusCode) else {
throw AppError.networkError(statusCode: http.statusCode)
}
return try decoder.decode([Recipe].self, from: data)
}
}
Rules:
- Protocol-based services for testability
Sendable conformance on all service types
- Typed errors with
LocalizedError
- No third-party HTTP libraries unless justified (URLSession is sufficient)
- Certificate pinning for sensitive data
Security
- Keychain for credentials, tokens, secrets — never UserDefaults
- App Transport Security enabled — HTTPS only
- No sensitive data in logs or crash reports
@AppStorage only for non-sensitive user preferences
- Input validation on all user-provided data
- Privacy manifest (
PrivacyInfo.xcprivacy) for App Store compliance
Accessibility
- Every interactive element needs an
accessibilityLabel
- Use semantic SwiftUI elements (Button, Toggle, Picker) — not
.onTapGesture
- Support Dynamic Type — no hardcoded font sizes
- Minimum tap target 44x44pt
- Test with VoiceOver before shipping
Deep References
See references/ for detailed guidance:
references/swiftui-patterns.md — Advanced view patterns, custom layouts, animations
references/concurrency-guide.md — Actor isolation, Sendable, structured concurrency
references/xcode-claude-integration.md — XcodeBuildMCP setup, hooks, sandbox modes
references/migration-guide.md — UIKit → SwiftUI, CoreData → SwiftData paths
Converted and distributed by TomeVault — claim your Tome and manage your conversions.
1---2name: apple-dev-best-practices3description: Apple platform development best practices for Swift 6, SwiftUI, SwiftData, and iOS/macOS apps. Use when building any iOS or macOS app, writing Swift code, designing SwiftUI views, working with Xcode projects, implementing navigation, state management, concurrency, networking, persistence, or testing on Apple platforms. Triggers on Swift, SwiftUI, iOS, macOS, Xcode, UIKit, SwiftData, Core Data, XCTest, StoreKit, CloudKit, MapKit, HealthKit, or any Apple framework. Also use when reviewing Swift code, debugging iOS apps, migrating UIKit to SwiftUI, or planning Apple platform architecture. Use when this capability is needed.4---56# Apple Development Best Practices78Modern Apple platform development using Swift 6 and SwiftUI as primary frameworks.910## When to Use1112Use this skill when:13- Building iOS or macOS apps with Swift 6 and SwiftUI14- Designing navigation, state management, or concurrency patterns for Apple platforms15- Working with SwiftData, Core Data, StoreKit, CloudKit, or other Apple frameworks16- Reviewing Swift code or planning Apple platform architecture1718## When NOT to Use1920Do NOT use this skill when:21- Building cross-platform mobile apps (Flutter, React Native, Kotlin Multiplatform) — use a cross-platform mobile persona instead, because Apple-specific patterns like `@Observable` and `NavigationStack` don't apply22- Writing server-side Swift (Vapor, Hummingbird) — use a backend engineering persona instead, because server-side Swift has different concurrency, deployment, and architecture concerns2324## Core Philosophy2526Build apps that are **previewable, testable, and maintainable**. A previewable app is a testable app. A testable app is a maintainable app.2728## Swift 6 Standards2930- **Strict concurrency** enabled — treat all warnings as errors31- **`@Observable`** over `ObservableObject` (iOS 17+)32- **`async/await`** for all asynchronous operations33- **Value types** (structs) preferred over reference types (classes) unless identity semantics needed34- **`guard`** for early exits, never deeply nested `if let` chains35- **Typed errors** via `LocalizedError` conformance — no raw strings36- **No force unwrapping** (`!`) without documented justification37- Follow Apple's Swift API Design Guidelines for naming3839## SwiftUI Architecture4041### State Management — Single Source of Truth (SSOT)4243```swift44// Local view state → @State45@State private var isExpanded = false4647// Observable model → @State with @Observable class48@State private var viewModel = RecipeViewModel()4950// Shared across view tree → @Environment51@Environment(\.recipeStore) private var store5253// Bindings to @Observable → @Bindable54@Bindable var viewModel: RecipeViewModel55```5657**Rules:**58- `@State` for view-local state only — never shared across views59- `@Observable` classes for ViewModels (replaces `ObservableObject` + `@Published`)60- `@Environment` for dependency injection (services, stores, settings)61- Never pass view models more than 2 levels deep — use Environment instead6263### Navigation — NavigationStack with Type-Safe Routing6465```swift66enum Route: Hashable {67 case recipeDetail(Recipe)68 case settings69 case profile(User)70}7172@Observable73final class Router {74 var path = NavigationPath()7576 func navigate(to route: Route) {77 path.append(route)78 }79}80```8182**Rules:**83- `NavigationStack` only — never deprecated `NavigationView`84- Type-safe routing via `Hashable` enum85- Router as `@Observable` class in `@Environment`86- Sheet presentation via optional ViewModel on parent8788### View Composition8990- Extract subviews at **50+ lines** or when reusable91- Max **100 lines** per view file before mandatory extraction92- Custom ViewModifiers for shared styling — not repeated inline styles93- Never use `AnyView` — destroys diffing performance and identity94- Prefer `@ViewBuilder` closures over `AnyView` for type erasure9596### Performance9798- **`LazyVStack`/`LazyHStack`** inside ScrollView — never eager stacks for large lists99- **`EquatableView`** wrapper for complex views that rarely change100- Keep view body **pure** — no side effects, no network calls101- Use `.task` modifier for async work, not `onAppear` with Task102- Profile with SwiftUI Performance Instrument (Xcode 16+)103104## Project Structure105106```107AppName/108├── App/ # App entry, lifecycle, configuration109│ ├── AppNameApp.swift110│ └── AppDelegate.swift # Only if needed for UIKit integration111├── Features/ # Feature modules (self-contained)112│ ├── Recipes/113│ │ ├── Views/ # SwiftUI views114│ │ ├── ViewModels/ # @Observable classes115│ │ └── Models/ # Data models (structs)116│ ├── MealPlanning/117│ └── Community/118├── Core/ # Shared infrastructure119│ ├── Extensions/120│ ├── Services/ # Networking, auth, analytics121│ ├── Persistence/ # SwiftData / Core Data122│ └── Components/ # Reusable UI components123├── Resources/ # Assets, Localizations, Fonts124└── Tests/125 ├── UnitTests/ # ViewModel + Service tests126 └── UITests/ # Critical user flow tests127```128129**Rules:**130- Features are **self-contained** — no cross-feature imports131- Shared code lives in `Core/` only132- Each feature has its own Views, ViewModels, Models133- Feature folders mirror navigation structure134135## Concurrency136137```swift138// Actor for thread-safe shared state139actor RecipeStore {140 private var cache: [UUID: Recipe] = [:]141142 func recipe(for id: UUID) -> Recipe? {143 cache[id]144 }145}146147// @MainActor for UI-bound classes148@MainActor149@Observable150final class RecipeListViewModel {151 var recipes: [Recipe] = []152 var isLoading = false153154 func loadRecipes() async {155 isLoading = true156 defer { isLoading = false }157 recipes = await recipeService.fetchAll()158 }159}160```161162**Rules:**163- `@MainActor` on all ViewModels164- `actor` for shared mutable state165- `Sendable` conformance for types crossing isolation boundaries166- Never `DispatchQueue.main.async` — use `@MainActor` instead167- `Task` only inside `.task` modifier or explicit user-initiated actions168- `TaskGroup` for parallel independent work169170## Testing171172- **Swift Testing** (`@Test`, `#expect`) preferred over XCTest for new code173- **Unit tests** for all ViewModel logic — 80%+ coverage on business logic174- **UI tests** for critical user flows only (login, purchase, core CRUD)175- **Dependency injection** via protocols for testability176- **No singletons** in production code — inject via `@Environment`177- Preview-driven development: if a view is hard to preview, it's hard to test178179## Persistence180181**SwiftData** (iOS 17+) is the default persistence layer:182183```swift184@Model185final class Recipe {186 var name: String187 var ingredients: [String]188 var instructions: String189 @Relationship(deleteRule: .cascade)190 var steps: [CookingStep]191}192```193194**Rules:**195- `@Model` classes for SwiftData — not structs196- Define `@Relationship` explicitly with delete rules197- Use `@Query` in views for automatic updates198- ModelContainer configured in App entry point199- Migration strategy documented before schema changes200201## Networking202203```swift204protocol RecipeServiceProtocol: Sendable {205 func fetchAll() async throws -> [Recipe]206 func create(_ recipe: Recipe) async throws -> Recipe207}208209struct RecipeService: RecipeServiceProtocol {210 private let session: URLSession211 private let decoder: JSONDecoder212213 func fetchAll() async throws -> [Recipe] {214 let (data, response) = try await session.data(from: endpoint)215 guard let http = response as? HTTPURLResponse,216 (200...299).contains(http.statusCode) else {217 throw AppError.networkError(statusCode: http.statusCode)218 }219 return try decoder.decode([Recipe].self, from: data)220 }221}222```223224**Rules:**225- Protocol-based services for testability226- `Sendable` conformance on all service types227- Typed errors with `LocalizedError`228- No third-party HTTP libraries unless justified (URLSession is sufficient)229- Certificate pinning for sensitive data230231## Security232233- **Keychain** for credentials, tokens, secrets — never UserDefaults234- **App Transport Security** enabled — HTTPS only235- No sensitive data in logs or crash reports236- `@AppStorage` only for non-sensitive user preferences237- Input validation on all user-provided data238- Privacy manifest (`PrivacyInfo.xcprivacy`) for App Store compliance239240## Accessibility241242- Every interactive element needs an `accessibilityLabel`243- Use semantic SwiftUI elements (Button, Toggle, Picker) — not `.onTapGesture`244- Support Dynamic Type — no hardcoded font sizes245- Minimum tap target 44x44pt246- Test with VoiceOver before shipping247248## Deep References249250See `references/` for detailed guidance:251- `references/swiftui-patterns.md` — Advanced view patterns, custom layouts, animations252- `references/concurrency-guide.md` — Actor isolation, Sendable, structured concurrency253- `references/xcode-claude-integration.md` — XcodeBuildMCP setup, hooks, sandbox modes254- `references/migration-guide.md` — UIKit → SwiftUI, CoreData → SwiftData paths255256---257> Converted and distributed by [TomeVault](https://tomevault.io/claim/aretedriver) — claim your Tome and manage your conversions.258<!-- tomevault:4.0:skill_md:2026-04-13 -->