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
@ObservableandNavigationStackdon'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
@ObservableoverObservableObject(iOS 17+)async/awaitfor all asynchronous operations- Value types (structs) preferred over reference types (classes) unless identity semantics needed
guardfor early exits, never deeply nestedif letchains- Typed errors via
LocalizedErrorconformance — 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:
@Statefor view-local state only — never shared across views@Observableclasses for ViewModels (replacesObservableObject+@Published)@Environmentfor 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:
NavigationStackonly — never deprecatedNavigationView- Type-safe routing via
Hashableenum - Router as
@Observableclass 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
@ViewBuilderclosures overAnyViewfor type erasure
Performance
LazyVStack/LazyHStackinside ScrollView — never eager stacks for large listsEquatableViewwrapper for complex views that rarely change- Keep view body pure — no side effects, no network calls
- Use
.taskmodifier for async work, notonAppearwith 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:
@MainActoron all ViewModelsactorfor shared mutable stateSendableconformance for types crossing isolation boundaries- Never
DispatchQueue.main.async— use@MainActorinstead Taskonly inside.taskmodifier or explicit user-initiated actionsTaskGroupfor 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:
@Modelclasses for SwiftData — not structs- Define
@Relationshipexplicitly with delete rules - Use
@Queryin 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
Sendableconformance 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
@AppStorageonly 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, animationsreferences/concurrency-guide.md— Actor isolation, Sendable, structured concurrencyreferences/xcode-claude-integration.md— XcodeBuildMCP setup, hooks, sandbox modesreferences/migration-guide.md— UIKit → SwiftUI, CoreData → SwiftData paths
