Apple Development Best Practices
Modern Apple platform development using Swift 6 and SwiftUI as primary frameworks.
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)
swift
// 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
swift
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 lists - •
EquatableViewwrapper 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
code
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
swift
// 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 ViewModels - •
actorfor shared mutable state - •
Sendableconformance for types crossing isolation boundaries - •Never
DispatchQueue.main.async— use@MainActorinstead - •
Taskonly inside.taskmodifier or explicit user-initiated actions - •
TaskGroupfor 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:
swift
@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
swift
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, 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