Jul 28, 2026

The Complete Guide to the Apple Development Ecosystem

A panoramic guide from Swift to Apple’s modern native frameworks, helping beginners build a complete mental model from first steps to shipping independently.

From zero to shipping solo, this is a practical map of Swift and Apple's modern native frameworks, not an API reference. Read chapters 1–3 in order, then use the rest and appendix as needed, consulting the Apple Developer Documentation for details. Updated in July 2026 for WWDC26: iOS 27 / macOS 27, Xcode 27, Swift 6.4, and SF Symbols 8 are still in beta. Learn with the current Xcode 26.5 / Swift 6.3 toolchain and treat those features as previews until fall.

Part I · Getting started

1. Environment and mindset

1.1 What you need

ItemRequirementNotes
MacApple Silicon (M series) requiredXcode 27 ships as an arm64-only binary and cannot be installed on an Intel Mac. macOS 26 Tahoe is the last major release line that supports Intel
OSShipping path: macOS Tahoe 26.2+ (with Xcode 26.5)<br>Beta path: macOS Tahoe 26.4+ (with Xcode 27 beta)Always check the live table on SDK and system requirements — it changes with every point release
XcodeShipping version from the Mac App Store (currently 26.5, Swift 6.3)<br>Xcode 27 beta from the developer downloads pageIncludes the compiler, simulators, Instruments, and previews
Developer accountA free account is enough to startOn-device debugging works, with limits: signing certificates and profiles need rebuilding roughly every 7 days, and there are caps on App IDs, registered devices, and apps per device (see membership comparison). Shipping to the App Store requires the Apple Developer Program at $99/year
HardwareA real device helps a lotThe simulator can't test the camera, sensors, some Live Activity behavior, or what biometrics actually feel like

Advice for solo developers: spend 2–3 weeks with a free account and the shipping Xcode first. Confirm you actually want to do this before paying the $99. Don't learn on a beta Xcode — beta bugs make it impossible to tell whether you wrote something wrong or the tool is broken. Upgrade once the fall release lands.

1.2 The Xcode surfaces that matter

  • Project Navigator (left, ⌘1): the file tree.
  • Canvas / Preview (right): live SwiftUI preview. You'll live in this while building UI; ⌥⌘P refreshes it.
  • Inspector (right, ⌥⌘0): attribute panel.
  • Console (bottom, ⇧⌘C): print output and error logs.
  • Scheme selector (top): pick what runs where (which app, which device).

Shortcuts worth memorizing: ⌘R run, ⌘. stop, ⌘B build, ⌃⌘←/→ navigate back/forward, ⇧⌘O open quickly, ⌘⇧A code actions menu.

1.3 Xcode's AI assistance

Two things get conflated here:

  • Predictive code completion: runs on a local Apple Silicon model and completes as you type. This is the part that involves the Neural Engine.
  • Coding agent: introduced in Xcode 27, driven by a model of your choice (external models supported), and capable of reading and writing across your whole project in multiple steps. It does not depend on the local Neural Engine.

Xcode 27 also ships a set of official agent skills for SwiftUI: they teach the agent SwiftUI's conventions and this year's new APIs directly, so generated code follows current best practice rather than whatever was in the training data three years ago.

What this means for a beginner: you can let it write, but you must be able to read what it wrote. The job of this guide is to give you that ability to read and judge. Don't skip the fundamentals and let AI generate everything — you'll end up with a project you can't maintain.

1.4 Mental model: how the ecosystem is organized

Think of it as four layers:

┌─────────────────────────────────────────────┐
│  Your app                                    │
├─────────────────────────────────────────────┤
│  Interface   SwiftUI · WidgetKit · RealityKit│
├─────────────────────────────────────────────┤
│  Capability  SwiftData · StoreKit · HealthKit│
│              FoundationModels · MapKit · ... │
├─────────────────────────────────────────────┤
│  Foundation  Swift stdlib · Foundation       │
│              Observation · Swift Concurrency │
└─────────────────────────────────────────────┘
  • The foundation layer is the language itself, and it is cross-platform. Swift and swift-foundation are quite mature on Linux; Android support is newer and narrower. Neither means "exactly the same API surface as on macOS."
  • The capability layer wraps Apple's system services. Almost all of it follows one pattern: declare a type, conform to a protocol, and the system calls you.
  • The interface layer is declarative: you describe what the UI should look like, and the framework figures out how to get there.

Once "declarative, protocol-driven, value types first" clicks, Apple's framework design becomes highly predictable.


2. Learning roadmap

Organized by stage. Each stage gives you a goal, content, and an artifact. The artifact matters — you cannot learn this without building things.

Stage 0: one week · get a feel for it

  • Goal: understand the full loop from writing code to seeing a UI.
  • Content: install Xcode; write print statements, variables, and loops in Swift Playground (or an Xcode playground); work through the first chapter of Apple's SwiftUI Tutorials.
  • Artifact: a screen showing your name and an image.

Stage 1: three to four weeks · the Swift language

  • Goal: be able to read any piece of Swift code on your own.
  • Content: chapters 3 and 4 here. Focus on optionals, value vs. reference types, protocols and extensions, and closures. Generics and macros only need conceptual familiarity for now.
  • Artifact: a pure-logic command-line program or playground — say, a to-do data model with create/read/update/delete.

Stage 2: four to six weeks · SwiftUI

  • Goal: get the interface in your head onto the screen, and make it respond to data.
  • Content: chapters 7–10. Focus on state management (@State / @Binding / @Observable / @Environment) and layout.
  • Artifact: a multi-screen app with navigation and in-memory data. An expense tracker that can't save yet, for example.

Stage 3: two to three weeks · persistence

  • Goal: close the app, reopen it, data is still there.
  • Content: chapters 11 and 12. SwiftData is the main line; @AppStorage handles lightweight settings.
  • Artifact: add persistence to the stage 2 app, then add iCloud sync.

Stage 4: three to four weeks · system integration

  • Goal: your app stops being an island.
  • Content: whichever parts of chapters 15, 17, and 18 apply to your app — widgets, notifications, sharing, App Intents/Siri.
  • Artifact: a Lock Screen widget plus a Siri shortcut for your app.

Stage 5: two weeks · monetization and shipping

  • Goal: actually put something on the App Store.
  • Content: chapters 22 and 23. StoreKit 2, TestFlight, App Store Connect.
  • Artifact: a shipped app, even if it only gets ten downloads.

Stage 6: ongoing · expand as needed

  • Intelligence features → chapters 14 and 16 (Foundation Models / Core AI)
  • Spatial computing → chapter 20 (RealityKit / visionOS)
  • Graphics and games → chapter 19
  • Multi-platform expansion → chapter 21

About the timeline: these estimates assume about two hours a day. Full-time will be twice as fast; strictly-evenings may be twice as slow. Don't compare your pace to anyone else's — compare output.


Part II · The language

3. Swift fundamentals

Swift was designed to be safe, fast, and expressive. The most beginner-friendly consequence: most mistakes are caught at compile time, not as a crash at runtime.

3.1 Variables and constants

swift
let name = "Jerry"        // constant, can't change
var count = 0             // variable, can change
count += 1

let pi: Double = 3.14159  // explicit type annotation
let flag = true           // inferred as Bool

Habit: write let by default. When you need to mutate, the compiler will tell you to change it to var. This isn't fastidiousness — it makes "what can change here" visible at a glance.

3.2 Basic types

swift
let i: Int = 42
let d: Double = 3.14
let s: String = "hello"
let b: Bool = true
let arr: [Int] = [1, 2, 3]                    // array
let dict: [String: Int] = ["a": 1, "b": 2]    // dictionary
let set: Set<Int> = [1, 2, 3]                 // set: unordered, unique
let tuple: (name: String, age: Int) = ("Jerry", 30)  // tuple

String interpolation:

swift
let age = 30
print("My name is \(name) and I'm \(age)")

Multi-line strings:

swift
let text = """
    first line
    second line
    """

3.3 Optionalsthe most important concept in Swift

"This value might not exist" is part of the type system, written T?.

swift
var nickname: String? = nil    // no nickname yet
nickname = "Jer"               // now there is one

You cannot use an optional directly. You have to handle the "doesn't exist" case first. Three ways:

swift
// 1. if let — enter the branch only if there's a value
if let nickname {
    print("Nickname is \(nickname)")   // here nickname is String, not String?
} else {
    print("No nickname")
}

// 2. guard let — bail out early if there's no value (preferred at the top of a function)
func greet(_ nickname: String?) {
    guard let nickname else {
        print("No nickname, can't greet")
        return
    }
    print("Hello, \(nickname)")     // unwrapped for the rest of the function
}

// 3. ?? — supply a default
let display = nickname ?? "Anonymous"

Optional chaining:

swift
let length = nickname?.count      // Int?; nil overall when nickname is nil

Force unwrapping with `!` deserves suspicion. nickname! crashes outright when the value is nil. Use it only when you are certain there's a value (you just checked, for instance); otherwise use one of the three forms above.

3.4 Control flow

swift
// if / else
if count > 10 { print("many") } else { print("few") }

// for-in
for i in 1...5 { print(i) }        // 1,2,3,4,5 (closed range)
for i in 1..<5 { print(i) }        // 1,2,3,4 (half-open range)
for item in arr { print(item) }
for (key, value) in dict { print("\(key)=\(value)") }

// while
while count < 10 { count += 1 }

// switch — Swift's switch must be exhaustive
let score = 85
switch score {
case 90...100: print("excellent")
case 60..<90:  print("pass")
default:       print("fail")
}

switch supports pattern matching, which is one of Swift's real strengths:

swift
let point = (x: 1, y: 0)
switch point {
case (0, 0):            print("origin")
case (_, 0):            print("on the X axis")
case (0, _):            print("on the Y axis")
case let (x, y) where x == y: print("on the diagonal")
default:                print("somewhere else")
}

3.5 Functions and closures

swift
// basic function
func add(_ a: Int, _ b: Int) -> Int {
    a + b        // single-expression functions can omit return
}

// argument labels: external name + internal name
func greet(person name: String, from city: String) -> String {
    "Hello \(name), from \(city)"
}
greet(person: "Jerry", from: "Beijing")

// default values
func makeCoffee(size: String = "medium", sugar: Int = 0) { }
makeCoffee()                  // uses defaults
makeCoffee(size: "large")

// variadic parameters
func sum(_ numbers: Int...) -> Int { numbers.reduce(0, +) }

// multiple return values via tuples
func minMax(_ arr: [Int]) -> (min: Int, max: Int)? {
    guard let first = arr.first else { return nil }
    return arr.reduce((first, first)) { (min($0.0, $1), max($0.1, $1)) }
}

A closure is a chunk of code you can pass around as a value. SwiftUI is full of them, so get comfortable.

swift
// full form
let double: (Int) -> Int = { (x: Int) -> Int in return x * 2 }
// shorter: type inference + implicit return
let double2: (Int) -> Int = { x in x * 2 }
// shortest: $0 for the first parameter
let double3: (Int) -> Int = { $0 * 2 }

// trailing closure: when the last parameter is a closure, write it outside the parens
let sorted = [3, 1, 2].sorted { $0 < $1 }
let doubled = [1, 2, 3].map { $0 * 2 }         // [2, 4, 6]
let evens = [1, 2, 3, 4].filter { $0 % 2 == 0 } // [2, 4]
let total = [1, 2, 3].reduce(0, +)              // 6

SwiftUI's Button is exactly this syntax:

swift
Button("Tap me") {          // this {} is the action closure
    print("tapped")
}

3.6 Structs, classes, and enums

These are Swift's three custom types. Rule of thumb first: use `struct` unless you need something only a `class` can do.

swift
// struct — value type, copied on assignment
struct Person {
    var name: String
    var age: Int

    // computed property
    var isAdult: Bool { age >= 18 }

    // method
    func greeting() -> String { "I'm \(name)" }

    // methods that mutate self must be marked mutating
    mutating func birthday() { age += 1 }
}

var a = Person(name: "Jerry", age: 30)
var b = a          // a copy; a and b are independent
b.name = "Tom"
print(a.name)      // still "Jerry"
swift
// class — reference type, shares the same object on assignment
class Counter {
    var value = 0
    func increment() { value += 1 }

    init(start: Int = 0) { value = start }   // classes need an init (unless every property has a default)
    deinit { print("deallocated") }
}

let c1 = Counter()
let c2 = c1        // c1 and c2 point at the same object
c2.increment()
print(c1.value)    // 1

When to use a class: you need inheritance, reference semantics (shared mutable state across several places), deinit, or a framework requires it (@Observable only works on classes, for instance).

swift
// enum — Swift's enums are unusually powerful
enum LoadState {
    case idle
    case loading
    case loaded(items: [String])    // associated values
    case failed(Error)
}

let state = LoadState.loaded(items: ["a", "b"])
switch state {
case .idle:              print("idle")
case .loading:           print("loading")
case .loaded(let items): print("loaded \(items.count) items")
case .failed(let error): print("failed: \(error)")
}

// raw-value enum
enum Direction: String, CaseIterable {
    case north = "N", south = "S", east = "E", west = "W"
}
Direction.allCases.forEach { print($0.rawValue) }

Enums with associated values are Swift's standard way to express a state machine, and much cleaner than a pile of booleans.

3.7 Property observers and lazy properties

swift
struct Settings {
    var volume: Double = 0.5 {
        willSet { print("about to change from \(volume) to \(newValue)") }
        didSet  { print("changed from \(oldValue) to \(volume)") }
    }

    lazy var expensiveThing = computeSomethingSlow()   // computed on first access
}

3.8 Error handling

swift
enum NetworkError: Error {
    case notFound
    case unauthorized
    case server(code: Int)
}

func fetch(id: Int) throws -> String {
    guard id > 0 else { throw NetworkError.notFound }
    return "data \(id)"
}

// callers must handle it
do {
    let data = try fetch(id: 1)
    print(data)
} catch NetworkError.notFound {
    print("not found")
} catch {
    print("other error: \(error)")
}

// try? — returns nil on failure
let data = try? fetch(id: -1)      // String?

// try! — crashes on failure; use sparingly

Make errors user-friendly:

swift
extension NetworkError: LocalizedError {
    var errorDescription: String? {
        switch self {
        case .notFound:      "Couldn't find that."
        case .unauthorized:  "Please sign in first."
        case .server(let c): "Server error (\(c))"
        }
    }
}

3.9 What's worth knowing about Swift 6.4

Swift 6.4 ships alongside Xcode 27, and both are currently in beta. The shipping toolchain (Xcode 26.5, and the standalone toolchain on swift.org) is Swift 6.3. You don't need these yet, but don't be confused when you see them:

swift
// 1. anyAppleOS — shorthand replacing all five platform names
@available(anyAppleOS 27, *)
func newFeature() { }

// previously you'd write:
// @available(iOS 27, macOS 27, watchOS 27, tvOS 27, visionOS 27, *)

// works in conditional compilation too
#if os(anyAppleOS)
    func makeWidget() -> some Widget { ... }
#endif

// carve out individual platforms as usual
@available(anyAppleOS 27, *)
@available(tvOS, unavailable)
func launch() { }

// 2. async code inside defer
func process() async throws {
    let handle = try await openResource()
    defer { await handle.close() }    // runs whether you return normally or throw
    try await doWork(with: handle)
}

// 3. @diagnose — finer-grained control over warnings

The rest (for-in iteration over noncopyable types like Span, URL parsing up to 4× faster, Swift Testing interoperating with XCTest) is under-the-hood work you'll benefit from automatically.


4. Swift beyond the basics: protocols, generics, macros

4.1 Protocols

A protocol defines what a type should be able to do. It's the skeleton of Apple's framework design — you will repeatedly meet the pattern "conform your type to this protocol, and the system will call it."

swift
protocol Drawable {
    var area: Double { get }        // requires a read-only property
    func draw()                     // requires a method
}

struct Circle: Drawable {
    let radius: Double
    var area: Double { .pi * radius * radius }
    func draw() { print("drawing a circle") }
}

Protocol extensions are Swift's killer feature — they let a protocol supply default implementations:

swift
extension Drawable {
    func describe() { print("a shape with area \(area)") }
    func draw() { print("default drawing") }    // conformers may skip draw
}

System protocols you'll meet constantly:

ProtocolWhat it gives you
Equatable== comparison
Hashableusable in a Set or as a dictionary key
Comparable<, >, sorting
Identifiablea unique id; required by SwiftUI's List/ForEach
Codableconversion to and from JSON and similar formats
Sendablesafe to pass across concurrency boundaries
CaseIterableiterate all cases of an enum

In most cases just declaring conformance is enough; the compiler synthesizes the implementation:

swift
struct Todo: Identifiable, Codable, Hashable {
    let id = UUID()
    var title: String
    var done = false
}
// That's it: JSON coding, deduplication, and SwiftUI lists all work now.

4.2 Extensions

Extensions add functionality to any type, including system types, without subclassing:

swift
extension String {
    var isValidEmail: Bool {
        contains("@") && contains(".")
    }
}
"a@b.com".isValidEmail    // true

extension Double {
    var asCurrency: String {
        formatted(.currency(code: "USD"))
    }
}

Extensions are also a good organizational tool — split a large type into several extensions by responsibility.

4.3 Generics

Generics let you write code that holds for any type:

swift
func firstElement<T>(of array: [T]) -> T? {
    array.first
}

// constrained generics
func maxElement<T: Comparable>(of array: [T]) -> T? {
    array.max()
}

// generic types
struct Stack<Element> {
    private var items: [Element] = []
    mutating func push(_ item: Element) { items.append(item) }
    mutating func pop() -> Element? { items.popLast() }
    var isEmpty: Bool { items.isEmpty }
}

var s = Stack<Int>()
s.push(1); s.push(2)

`some` vs. `any`:

swift
func makeShape() -> some Drawable { Circle(radius: 1) }
//                  ↑ "one specific type that I'm not naming" — resolved at compile time, fast

func processAny(_ shape: any Drawable) { shape.draw() }
//                       ↑ "any type conforming to this protocol" — dynamic dispatch, works in heterogeneous arrays

SwiftUI's var body: some View is the first form. You don't have to spell out the absurdly nested type; the compiler remembers it for you.

4.4 Macros

Swift macros generate code at compile time. You are mainly a consumer, not an author. The ones you'll use:

MacroFromWhat it does
@ObservableObservationmakes a class's property changes observable by SwiftUI
@ModelSwiftDatamakes a class a persistable model
@GenerableFoundationModelsmakes a type structurally generatable by an LLM
@Test / #expectSwift Testingdefines tests
#PreviewSwiftUIdefines a preview
#PredicateFoundationtype-safe query predicates

To see what a macro expands into: right-click the macro name → Expand Macro. It's a great way to understand framework behavior.

4.5 Memory management and reference cycles

Swift uses ARC (automatic reference counting) for class instances. You won't think about it 99% of the time, but one trap matters: two objects strongly referencing each other leak memory.

swift
class Parent { var child: Child? }
class Child { weak var parent: Parent? }    // weak breaks the cycle

Same story when a closure captures self:

swift
class ViewModel {
    var onUpdate: (() -> Void)?
    func setup() {
        onUpdate = { [weak self] in         // [weak self] avoids the cycle
            guard let self else { return }
            self.doSomething()
        }
    }
    func doSomething() {}
}

Use structs and this mostly stops being a concern — another reason for "structs first."


5. Swift concurrency

Modern Swift concurrency is async/await plus actors. Forget callback hell and `DispatchQueue` — those live in legacy code.

5.1 async / await

swift
func loadUser(id: Int) async throws -> User {
    let url = URL(string: "https://api.example.com/user/\(id)")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(User.self, from: data)
}

// calling it
Task {
    do {
        let user = try await loadUser(id: 1)
        print(user.name)
    } catch {
        print("failed: \(error)")
    }
}

await means "this may suspend, yielding the thread to someone else, and resume when it's done." The code reads synchronously and executes asynchronously.

5.2 Running things in parallel

swift
// serial: total time = A + B
let a = try await loadUser(id: 1)
let b = try await loadUser(id: 2)

// parallel: total time = max(A, B)
async let userA = loadUser(id: 1)
async let userB = loadUser(id: 2)
let (a2, b2) = try await (userA, userB)

// use a TaskGroup when the count isn't fixed
func loadAll(ids: [Int]) async throws -> [User] {
    try await withThrowingTaskGroup(of: User.self) { group in
        for id in ids {
            group.addTask { try await loadUser(id: id) }
        }
        var results: [User] = []
        for try await user in group { results.append(user) }
        return results
    }
}

5.3 Tasks and cancellation

swift
let task = Task {
    for i in 0..<1000 {
        try Task.checkCancellation()    // check whether we've been cancelled
        await process(i)
    }
}
task.cancel()

In SwiftUI, the .task {} modifier cancels automatically when the view disappears:

swift
struct UserView: View {
    @State private var user: User?

    var body: some View {
        Text(user?.name ?? "Loading")
            .task {                       // starts when the view appears, cancels when it goes away
                user = try? await loadUser(id: 1)
            }
    }
}

5.4 Actors and data isolation

An actor is a type that guarantees its internal state won't be corrupted by concurrent access:

swift
actor ImageCache {
    private var cache: [URL: Data] = [:]

    func image(for url: URL) -> Data? { cache[url] }
    func store(_ data: Data, for url: URL) { cache[url] = data }
}

let cache = ImageCache()
await cache.store(data, for: url)     // reaching an actor requires await

`@MainActor` is the one you'll use most — it represents the main thread. All UI updates must happen there:

swift
@MainActor
@Observable
final class TodoStore {
    var todos: [Todo] = []

    func refresh() async {
        let fetched = await fetchFromServer()   // off the main thread
        todos = fetched                          // back on the main thread to update
    }
}

SwiftUI's View is @MainActor by default, so updating @State inside body and .task is safe.

5.5 Sendable and strict concurrency

Strict concurrency checking is determined by the language mode, not by which compiler you installed. Xcode lets you pick between Swift 6 / Swift 5 / Swift 4.2 / Swift 4 language modes, so older projects can stay on Swift 5 mode and migrate gradually.

In Swift 6 language mode, strict checking is on by default: data crossing a concurrency boundary must be `Sendable` (safe to hand between tasks). New project templates default to Swift 6 mode, and you should keep it.

  • A struct or enum whose members are all value types or immutable → Sendable automatically
  • A class needs manual guarantees (final plus immutable properties, or @unchecked Sendable with your own locking)
  • An actor is inherently Sendable
swift
struct Todo: Sendable { ... }              // usually inferred; you rarely write this

final class Config: Sendable {             // requires every property to be let and Sendable
    let apiKey: String
    init(apiKey: String) { self.apiKey = apiKey }
}

Advice for beginners: when you hit "Sending value of non-Sendable type…", first ask whether this data really needs to cross threads. The right answer is often to mark the type @MainActor rather than force it through with @unchecked Sendable.

5.6 AsyncSequence

A series of values arriving over time:

swift
// read a file line by line
for try await line in url.lines {
    print(line)
}

// custom stream
let stream = AsyncStream<Int> { continuation in
    Task {
        for i in 0..<10 {
            continuation.yield(i)
            try? await Task.sleep(for: .seconds(1))
        }
        continuation.finish()
    }
}
for await value in stream { print(value) }

StoreKit's Transaction.updates and Foundation Models' streaming output are both AsyncSequences.


6. Swift Package Manager

SPM is Apple's official dependency manager, deeply integrated into Xcode, and the default choice for new projects. CocoaPods and Carthage are third-party tools with plenty of existing projects still on them, but there's no reason to bring them into something new.

6.1 Adding a dependency

In Xcode: File → Add Package Dependencies…, paste the GitHub URL.

6.2 Creating your own package

Extracting reusable code into a local package is the best way to keep a large project tidy.

The example below targets the current shipping toolchain (Xcode 26.5 / Swift 6.3). The versions available in swift-tools-version and platforms depend on your installed toolchain — .v27 requires Xcode 27.

swift
// Package.swift
// swift-tools-version: 6.2
import PackageDescription

let package = Package(
    name: "MyKit",
    platforms: [.iOS(.v26), .macOS(.v26)],   // .v27 becomes available with the Xcode 27 toolchain
    products: [
        .library(name: "MyKit", targets: ["MyKit"])
    ],
    dependencies: [],
    targets: [
        .target(name: "MyKit"),
        .testTarget(name: "MyKitTests", dependencies: ["MyKit"])
    ]
)

Command line:

bash
swift package init --type library
swift build
swift test
swift run

6.3 Packages worth knowing

  • Swift Package Index — now part of Apple, and the authoritative place to find Swift packages
  • swift-collectionsDeque, OrderedSet, and other containers the stdlib lacks
  • swift-algorithmschunked, windows, and other sequence algorithms
  • swift-async-algorithms — composition operators for AsyncSequence
  • foundation-models-utilities — new at WWDC26, tooling for LLM workflows

Advice for solo developers: in the Apple ecosystem, fewer third-party dependencies is better. System framework coverage is extremely high, and every package you add is potential maintenance cost the next time the OS updates.


Part III · Interface

7. SwiftUI core

SwiftUI is Apple's unified declarative UI framework across all platforms. One codebase, six platforms (iOS / iPadOS / macOS / watchOS / tvOS / visionOS). It's the only UI framework a beginner should learn.

7.1 Declarative thinking

Imperative (the old UIKit way): "create a label, set its text, add it to a superview, and when the data changes, find that label and change its text."

Declarative (SwiftUI): "there is a label on screen whose text always equals count." When the data changes, the UI follows; you don't write the update code.

swift
struct CounterView: View {
    @State private var count = 0

    var body: some View {
        VStack(spacing: 16) {
            Text("Count: \(count)")       // always equals count
                .font(.largeTitle)
            Button("Add one") { count += 1 }  // change data only; the UI updates itself
        }
    }
}

7.2 The View protocol

Every UI element conforms to View, which requires exactly one thing — a body:

swift
struct MyView: View {
    var body: some View {
        Text("Hello")
    }
}

The key insight: body isn't "the interface," it's a recipe for the interface. SwiftUI calls body repeatedly as data changes, diffs the old recipe against the new one, and updates only what actually changed. Therefore:

  • Don't do expensive work inside body
  • Don't cause side effects inside body (no network requests, no database writes)
  • Rebuilding a struct is extremely cheap; don't worry about the performance of that

7.3 Common view components

swift
// text
Text("Title")
    .font(.title)
    .fontWeight(.semibold)
    .foregroundStyle(.secondary)

// images
Image(systemName: "star.fill")        // SF Symbols, 7000+ built-in icons
    .symbolRenderingMode(.multicolor)
Image("myPhoto")                       // from Assets
    .resizable()
    .scaledToFit()

// remote images (HTTP caching by default as of iOS 27)
AsyncImage(url: url) { image in
    image.resizable().scaledToFill()
} placeholder: {
    ProgressView()
}

// buttons
Button("OK") { }
Button("Delete", systemImage: "trash", role: .destructive) { }

// input
@State var text = ""
TextField("Enter something", text: $text)
SecureField("Password", text: $password)
TextEditor(text: $longText)

// selection
Toggle("Enable notifications", isOn: $notificationsEnabled)
Slider(value: $volume, in: 0...1)
Stepper("Quantity: \(count)", value: $count, in: 1...10)
Picker("Size", selection: $size) {
    Text("Small").tag(Size.small)
    Text("Medium").tag(Size.medium)
}
DatePicker("Date", selection: $date, displayedComponents: .date)
ColorPicker("Color", selection: $color)

// status
ProgressView()                          // spinner
ProgressView(value: 0.7)                // progress bar
Label("Favorite", systemImage: "heart")
Link("Visit website", destination: url)
ShareLink(item: url)                    // system share sheet

7.4 Modifiers

A modifier returns a new view wrapping the original. So order matters:

swift
Text("Hi")
    .padding()              // padding first
    .background(.blue)      // then background → background includes the padding

Text("Hi")
    .background(.blue)      // background first → only as big as the text
    .padding()              // then padding → padding sits outside the background

Common modifiers by category:

swift
// size and position
.frame(width: 100, height: 50)
.frame(maxWidth: .infinity)             // take as much as available
.padding(.horizontal, 16)
.offset(x: 10, y: 0)

// appearance
.background(.regularMaterial)           // material (frosted glass) background
.foregroundStyle(.blue)
.clipShape(.rect(cornerRadius: 12))
.overlay { RoundedRectangle(cornerRadius: 12).stroke(.gray) }
.shadow(radius: 4)
.opacity(0.8)

// interaction
.onTapGesture { }
.disabled(isLoading)
.contextMenu { Button("Delete") { } }
.swipeActions { Button("Archive") { } }

// lifecycle
.task { await load() }                  // async work when the view appears
.onAppear { }
.onChange(of: query) { old, new in }

// presentation
.sheet(isPresented: $showSheet) { DetailView() }
.sheet(item: $selectedItem) { item in DetailView(item: item) }
.fullScreenCover(isPresented: $show) { }
.alert("Delete this?", isPresented: $showAlert) {
    Button("Delete", role: .destructive) { }
    Button("Cancel", role: .cancel) { }
}
.confirmationDialog("Choose an action", isPresented: $showDialog) { }
.popover(isPresented: $showPopover) { }
New in iOS 27
alert and confirmationDialog now support the same item: binding style as sheets — set the bound value and the presentation appears, no separate Bool to maintain.

7.5 Containers and lists

swift
// stacks
VStack(alignment: .leading, spacing: 8) { }    // vertical
HStack { }                                      // horizontal
ZStack(alignment: .topTrailing) { }             // layered

// scrolling
ScrollView { LazyVStack { } }
ScrollView(.horizontal) { LazyHStack { } }

// grids
LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))]) { }

// lists — the workhorse
List(todos) { todo in
    Text(todo.title)
}

// with sections, deletion, and reordering
List {
    Section("Today") {
        ForEach(todos) { todo in
            TodoRow(todo: todo)
        }
        .onDelete { indexSet in delete(at: indexSet) }
        .onMove { from, to in move(from: from, to: to) }
    }
}

// tables (macOS / iPadOS)
Table(people) {
    TableColumn("Name", value: \.name)
    TableColumn("Age") { Text("\($0.age)") }
}
New in iOS 27: universal reordering. Drag-to-reorder used to be a List privilege; now LazyVGrid, LazyHStack, and arbitrary containers use the same API, and watchOS supports reordering for the first time. swipeActionsContainer enables swipe actions across an entire ScrollView.

7.6 Previews

swift
#Preview {
    CounterView()
}

#Preview("Dark mode") {
    CounterView()
        .preferredColorScheme(.dark)
}

#Preview("With data", traits: .sizeThatFitsLayout) {
    TodoRow(todo: .init(title: "Buy milk"))
}

Previews are the core of SwiftUI's development speed. Get in the habit of writing one for every view and your iteration loop gets several times faster.


8. SwiftUI state management

This is where beginners get most confused. Hold onto one thread: there is exactly one source of truth for each piece of data, and everything else is a reference to it.

8.1 Five property wrappers

WrapperUsed forWho owns the data
@Statesimple state private to a viewthe current view
@Bindinga read-write reference passed down from a parentthe parent view
@Environmentshared data read from the environmentan ancestor view or the system
@Bindablegetting bindings from an @Observable objectsomewhere else
@AppStoragea setting synced with UserDefaultssystem preferences

8.2 @State and @Binding

swift
struct ParentView: View {
    @State private var isOn = false          // source of truth lives here

    var body: some View {
        VStack {
            Text(isOn ? "On" : "Off")
            ChildToggle(isOn: $isOn)          // $ produces a Binding
        }
    }
}

struct ChildToggle: View {
    @Binding var isOn: Bool                   // references the parent's state

    var body: some View {
        Toggle("Switch", isOn: $isOn)
    }
}

Always mark @State private — it's an implementation detail of the view.

Change in Xcode 27
State became a macro. Class instances stored in @State are now lazily initialized and created exactly once per view lifetime. The old problem of @State initial values being repeatedly constructed is handled automatically. Note this is toolchain behavior, not device behavior: compile with Xcode 27 and it applies back to the OS versions that introduced @Observable (the iOS 17 / macOS 14 line), not only on iOS 27 devices.

8.3 @Observablethe modern data model

This macro comes from the Observation framework, and it replaces the older `ObservableObject` / `@Published` / `@StateObject` / `@ObservedObject` combination.

swift
import Observation

@MainActor
@Observable
final class TodoStore {
    var todos: [Todo] = []
    var isLoading = false

    // properties you don't want observed
    @ObservationIgnored private var cache: [String: Data] = [:]

    func add(_ title: String) {
        todos.append(Todo(title: title))
    }

    func load() async {
        isLoading = true
        defer { isLoading = false }
        todos = await fetchTodos()
    }
}

Using it:

swift
struct TodoListView: View {
    @State private var store = TodoStore()      // held with @State, not @StateObject

    var body: some View {
        List(store.todos) { todo in
            Text(todo.title)
        }
        .task { await store.load() }
    }
}

Why `@Observable` wins: it redraws only when a property the body actually read changes. The old ObservableObject redrew the whole view on any @Published change, which performed far worse.

When you need two-way bindings to an @Observable object's properties, use @Bindable:

swift
struct EditView: View {
    @Bindable var todo: Todo        // Todo is an @Observable class

    var body: some View {
        TextField("Title", text: $todo.title)
    }
}

8.4 @Environment

For passing data through many layers without threading it manually:

swift
// 1. define (the iOS 17+ macro form)
extension EnvironmentValues {
    @Entry var todoStore = TodoStore()
}

// 2. inject
@main
struct MyApp: App {
    @State private var store = TodoStore()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.todoStore, store)
        }
    }
}

// 3. read
struct DeepChildView: View {
    @Environment(\.todoStore) private var store
    var body: some View { Text("\(store.todos.count)") }
}

There's a shorter form for @Observable types:

swift
ContentView().environment(store)                 // inject
@Environment(TodoStore.self) private var store   // read

The system also provides plenty of environment values:

swift
@Environment(\.colorScheme) private var colorScheme          // light/dark
@Environment(\.dismiss) private var dismiss                  // close the current screen
@Environment(\.openURL) private var openURL
@Environment(\.horizontalSizeClass) private var sizeClass    // compact/regular layout
@Environment(\.dynamicTypeSize) private var typeSize         // the user's text size setting
@Environment(\.scenePhase) private var scenePhase            // foreground/background
@Environment(\.locale) private var locale

8.5 @AppStorage

The simplest option for lightweight settings:

swift
struct SettingsView: View {
    @AppStorage("username") private var username = ""
    @AppStorage("isDarkMode") private var isDarkMode = false
    @AppStorage("fontSize") private var fontSize = 16.0

    var body: some View {
        Form {
            TextField("Username", text: $username)
            Toggle("Dark mode", isOn: $isDarkMode)
        }
    }
}

Values land in UserDefaults and survive relaunch. Only for small data (settings, switches, the last selected tab) — don't store business data here.

8.6 A decision tree for state

Used in one view, and simple?
  → @State

Needs to be passed to a child that can modify it?
  → @Binding

Has business logic, shared across views?
  → @Observable class + held with @State + distributed via @Environment

Just a user preference?
  → @AppStorage

Business data that needs to persist?
  → SwiftData (chapter 11)

Don't over-engineer. Plenty of tutorials will push MVVM, Clean Architecture, or Redux at you. As a solo developer, build the simplest thing that works first. When a view's body passes 100 lines, or state logic starts repeating, that's soon enough to extract an @Observable model.


9. Navigation, layout, and animation

9.1 Navigation

swift
// NavigationStack — single-column push navigation (the iPhone workhorse)
NavigationStack {
    List(todos) { todo in
        NavigationLink(todo.title, value: todo)
    }
    .navigationTitle("To-do")
    .navigationDestination(for: Todo.self) { todo in
        TodoDetailView(todo: todo)
    }
}

// programmatic navigation: drive it with a path array
@State private var path: [Todo] = []

NavigationStack(path: $path) {
    ...
}
// then path.append(todo) or path.removeAll() to jump / pop

// NavigationSplitView — two or three columns (iPad, Mac)
NavigationSplitView {
    SidebarView()          // sidebar
} content: {
    ListView()             // middle column (optional)
} detail: {
    DetailView()           // detail
}

// TabView — bottom tabs
TabView {
    Tab("Home", systemImage: "house") { HomeView() }
    Tab("Settings", systemImage: "gear") { SettingsView() }
    Tab(role: .search) { SearchView() }     // the search tab gets special styling
}

Cross-platform tip: write NavigationSplitView once. It degrades to push navigation on iPhone and expands to multiple columns on iPad and Mac.

9.2 Toolbars

swift
.toolbar {
    ToolbarItem(placement: .topBarLeading) {
        Button("Cancel") { dismiss() }
    }
    ToolbarItemGroup(placement: .primaryAction) {
        Button("Save") { save() }
        Menu("More") { ... }
    }
}
New toolbar controls in iOS 27 (described below by shape; check the Toolbars documentation for exact symbol names — these span modifiers, containers, and placements): - visibility priority — declare which toolbar groups survive when the window narrows - overflow menu — permanently move low-priority items into an overflow menu (a container form, e.g. ToolbarOverflowMenu { … }) - `.topBarPinnedTrailing` placement — pin critical actions (like share) to the trailing edge, used with ToolbarItem(placement:) - `.toolbarMinimizeBehavior(_:for:)` — collapse the navigation bar while scrolling Together these let one codebase produce sensible toolbars on iPhone, iPad, and Mac. Worth getting right from the start.
swift
@State private var query = ""

List(filteredItems) { ... }
    .searchable(text: $query, prompt: "Search to-dos")
    .searchSuggestions {
        ForEach(suggestions) { Text($0).searchCompletion($0) }
    }
    .searchScopes($scope) {
        Text("All").tag(Scope.all)
        Text("Pending").tag(Scope.pending)
    }

9.4 The layout system

SwiftUI layout is a three-step negotiation: parent proposes a size → child decides its own size → parent places the child. Understanding this resolves 90% of layout confusion.

swift
// Spacer pushes things apart
HStack {
    Text("left")
    Spacer()
    Text("right")
}

// layout priority
HStack {
    Text("a very long piece of text here").layoutPriority(1)
    Text("short")
}

// fixed size (won't be compressed)
Text("no wrapping").fixedSize()

// ViewThatFits picks the first option that fits
ViewThatFits {
    HStack { A(); B(); C() }      // use this when wide
    VStack { A(); B(); C() }      // use this when narrow
}

// GeometryReader reads available space (avoid when possible — it breaks the negotiation)
GeometryReader { proxy in
    Text("width: \(proxy.size.width)")
}

// Grid — for layouts that need alignment
Grid {
    GridRow { Text("Name"); Text("Jerry") }
    GridRow { Text("City"); Text("Beijing") }
}

9.5 Animation

swift
// implicit: state changes, transition happens
@State private var isExpanded = false

Rectangle()
    .frame(height: isExpanded ? 200 : 100)
    .animation(.spring, value: isExpanded)     // animate only changes to isExpanded

// explicit
Button("Expand") {
    withAnimation(.spring(duration: 0.4)) {
        isExpanded.toggle()
    }
}

// transitions: how a view appears and disappears
if showDetail {
    DetailView()
        .transition(.move(edge: .bottom).combined(with: .opacity))
}

// matched geometry: morph between two views
@Namespace private var namespace

// view A
Image(...).matchedGeometryEffect(id: "hero", in: namespace)
// view B
Image(...).matchedGeometryEffect(id: "hero", in: namespace)

// navigation transitions (iOS 18+): zoom from a card into a detail screen
NavigationLink { DetailView() } label: { CardView() }
    .matchedTransitionSource(id: item.id, in: namespace)

// phase animator: multi-step animation
Image(systemName: "bell")
    .phaseAnimator([0, -20, 20, 0]) { view, angle in
        view.rotationEffect(.degrees(angle))
    }

// keyframe animator: independent timelines per property
.keyframeAnimator(initialValue: AnimationValues()) { view, value in
    view.scaleEffect(value.scale).offset(y: value.offset)
} keyframes: { _ in
    KeyframeTrack(\.scale) {
        SpringKeyframe(1.2, duration: 0.2)
        SpringKeyframe(1.0, duration: 0.3)
    }
}

// SF Symbols effects
Image(systemName: "heart.fill")
    .symbolEffect(.bounce, value: likeCount)
    .contentTransition(.symbolEffect(.replace))

The principle: animation exists to explain a state change, not to decorate. Default to .spring — it feels the most natural on Apple platforms.

9.6 Gestures

swift
.onTapGesture(count: 2) { }
.onLongPressGesture { }

@State private var offset = CGSize.zero
.gesture(
    DragGesture()
        .onChanged { offset = $0.translation }
        .onEnded { _ in withAnimation { offset = .zero } }
)

@State private var scale = 1.0
.gesture(MagnifyGesture().onChanged { scale = $0.magnification })

// combining gestures
.gesture(dragGesture.simultaneously(with: magnifyGesture))

10. Design language and interface-adjacent frameworks

10.1 The Liquid Glass design language

Liquid Glass, introduced in iOS 26, is Apple's current cross-platform visual language: translucent, refractive material layers with content flowing beneath. iOS 27 refreshes those materials, refines the typography, and unifies the appearance of tab and navigation bars.

As a SwiftUI developer, you mostly do nothing — use the standard components (NavigationStack, TabView, .toolbar, Button) and you get the correct appearance automatically. When you need manual control:

swift
// apply the glass effect to a custom view
MyCustomBar()
    .glassEffect(in: .rect(cornerRadius: 20))

// material backgrounds
.background(.regularMaterial)      // also .thin / .thick / .ultraThin / .ultraThick

// button styles
Button("OK") { }.buttonStyle(.glass)
Button("OK") { }.buttonStyle(.borderedProminent)

The key principle: don't fight the system. A fully custom navigation bar you drew yourself will look out of place at the next OS update. The aesthetic payoff in the Apple ecosystem comes from deferring to the system and expressing your brand in the content area.

10.2 SF Symbols

A built-in vector icon library that adapts automatically to weight, size, and light/dark mode, with animation support. SF Symbols 8 (beta) includes 7000+ symbols.

Download the SF Symbols app to browse and search.

swift
Image(systemName: "heart.fill")
    .font(.title)
    .symbolRenderingMode(.hierarchical)      // hierarchical rendering
    .foregroundStyle(.red, .pink)            // multicolor
    .symbolVariant(.circle)                  // variants
    .symbolEffect(.pulse)                    // effects

10.3 Icon Composer

Apple's official app-icon tool: one design generates icons for every platform and every mode (light / dark / tinted / clear glass). Get it from the Apple Developer downloads. A big labor saver for solo developers.

10.4 Swift Charts

A declarative charting framework with syntax identical to SwiftUI's.

swift
import Charts

struct SalesChart: View {
    let data: [Sale]

    var body: some View {
        Chart(data) { sale in
            BarMark(
                x: .value("Month", sale.month),
                y: .value("Revenue", sale.amount)
            )
            .foregroundStyle(by: .value("Product", sale.product))
        }
        .chartXAxis { AxisMarks(values: .automatic) }
        .chartLegend(position: .bottom)
        .frame(height: 200)
    }
}

It supports BarMark / LineMark / AreaMark / PointMark / SectorMark (pie) / RuleMark and lets you layer them. For data-heavy apps it removes the need for a third-party chart library.

10.5 TipKit

The system's unified feature-discovery framework. Better than rolling your own coach marks — it handles display frequency, read state, and cross-device sync for you.

swift
import TipKit

struct FavoriteTip: Tip {
    var title: Text { Text("Save this article") }
    var message: Text? { Text("Tap the star to find it later in Favorites") }
    var image: Image? { Image(systemName: "star") }
}

// configure at app launch
try? Tips.configure()

// show it in the UI
Button("Favorite", systemImage: "star") { }
    .popoverTip(FavoriteTip())

10.6 Interoperating with UIKit / AppKit

New projects shouldn't use UIKit, but occasionally you need something SwiftUI hasn't covered:

swift
struct WebView: UIViewRepresentable {
    let url: URL

    func makeUIView(context: Context) -> WKWebView { WKWebView() }
    func updateUIView(_ view: WKWebView, context: Context) {
        view.load(URLRequest(url: url))
    }
}

macOS uses NSViewRepresentable; going the other direction, use UIHostingController / NSHostingController.

When you genuinely need it: WKWebView, certain advanced AVPlayerViewController configurations, and some older third-party SDKs. Otherwise, look for the SwiftUI equivalent in the docs first.

10.7 Accessibility

Not optional. SwiftUI does most of the work by default; you only need to fill in custom elements:

swift
Image(systemName: "star.fill")
    .accessibilityLabel("Favorited")
    .accessibilityHint("Double tap to unfavorite")
    .accessibilityAddTraits(.isButton)

// merge a group of elements into one accessible element
HStack { Image(...); Text("Title"); Text("Subtitle") }
    .accessibilityElement(children: .combine)

Other essentials: never convey information by color alone; support Dynamic Type (use .font(.body), not .font(.system(size: 16))); respect the Reduce Motion setting.

10.8 Localization

Since Xcode 15, use String Catalogs (.xcstrings).

  1. Add a Localizable.xcstrings to the project
  2. Write Text("Hello") as normal; Xcode extracts strings automatically
  3. Add target languages and translate in the catalog editor
swift
Text("Hello")                             // localized automatically
Text("^[\(count) item](inflect: true)")   // plural variants, configured in the catalog
String(localized: "Confirm deletion", comment: "Title of the delete confirmation alert")

// formatting (follows the user's region automatically)
Text(date, format: .dateTime.year().month().day())
Text(price, format: .currency(code: "USD"))
Text(bytes, format: .byteCount(style: .file))

Part IV · Data

11. SwiftData

SwiftData is Apple's modern persistence framework, wrapping Core Data's capabilities with macros and Swift's type system. Don't use Core Data for new projects.

11.1 Defining a model

swift
import SwiftData

@Model
final class Todo {
    var title: String
    var isDone: Bool
    var createdAt: Date
    var priority: Priority

    // relationships
    @Relationship(deleteRule: .cascade)
    var subtasks: [Subtask] = []

    // not persisted
    @Transient var isEditing = false

    // uniqueness constraint
    #Unique<Todo>([\.title])

    // index for faster queries
    #Index<Todo>([\.createdAt])

    init(title: String, priority: Priority = .normal) {
        self.title = title
        self.isDone = false
        self.createdAt = .now
        self.priority = priority
    }
}

enum Priority: Int, Codable, CaseIterable {
    case low, normal, high
}

What the @Model macro does: turns the class into a persistable entity, implements Observable automatically (so SwiftUI can watch it), and generates the read/write logic for stored properties.

New in iOS 27
@Attribute(.codable) lets any Codable custom or third-party type be stored directly as a property, instead of being manually decomposed into primitive fields.

11.2 Configuring the container

swift
@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: Todo.self)      // one line
    }
}

For more control:

swift
let container = try ModelContainer(
    for: Todo.self, Subtask.self,
    configurations: ModelConfiguration(
        isStoredInMemoryOnly: false,
        cloudKitDatabase: .automatic      // turn on iCloud sync
    )
)

11.3 Queries

swift
struct TodoListView: View {
    // simplest: everything
    @Query private var todos: [Todo]

    // sorted
    @Query(sort: \Todo.createdAt, order: .reverse)
    private var recentTodos: [Todo]

    // filter + sort
    @Query(
        filter: #Predicate<Todo> { !$0.isDone },
        sort: [SortDescriptor(\.priority, order: .reverse),
               SortDescriptor(\.createdAt)]
    )
    private var pendingTodos: [Todo]

    var body: some View {
        List(pendingTodos) { todo in
            Text(todo.title)
        }
    }
}

#Predicate is type safe: misspell a property and it fails to compile, rather than blowing up at runtime.

New in iOS 27: sectioned queries. Pass a sectionBy: argument to @Query (a KeyPath from the model root to a string property) and get pre-grouped results, ready for Section — no more Dictionary(grouping:) in memory. The community has also mentioned enum predicates and composable predicates, but I couldn't confirm those as explicit additions in Apple's own "What's new in SwiftData" material. Check the documentation before relying on them.

Dynamic queries — where the filter depends on user input — need to be built in init:

swift
struct SearchableTodoList: View {
    @Query private var todos: [Todo]

    init(searchText: String) {
        _todos = Query(filter: #Predicate<Todo> {
            searchText.isEmpty || $0.title.localizedStandardContains(searchText)
        })
    }

    var body: some View { List(todos) { Text($0.title) } }
}

11.4 Create, update, delete

swift
struct AddTodoView: View {
    @Environment(\.modelContext) private var context
    @State private var title = ""

    var body: some View {
        Form {
            TextField("Title", text: $title)
            Button("Add") {
                context.insert(Todo(title: title))   // insert
                // no manual save() needed; SwiftUI saves automatically
            }
        }
    }
}

// delete
context.delete(todo)

// update: just mutate the property
todo.isDone = true

// manual save (needed outside a SwiftUI context)
try context.save()

// batch delete
try context.delete(model: Todo.self, where: #Predicate { $0.isDone })

11.5 Using SwiftData outside SwiftUI

Background tasks, command-line tools, and tests need a context you create yourself:

swift
@ModelActor
actor TodoImporter {
    func importTodos(from data: Data) throws {
        let items = try JSONDecoder().decode([TodoDTO].self, from: data)
        for item in items {
            modelContext.insert(Todo(title: item.title))
        }
        try modelContext.save()
    }
}

// usage
let importer = TodoImporter(modelContainer: container)
try await importer.importTodos(from: data)

The @ModelActor macro generates an actor bound to its own ModelContext, so background writes don't collide with the main thread.

New in iOS 27
ResultsObserver and HistoryObserver let you observe data changes anywhere — not just in SwiftUI views — to drive state objects or react to specific model updates. This fills SwiftData's most obvious previous gap.

11.6 Migrations

Changing the model's shape requires a migration. SwiftData handles lightweight changes (adding an optional property, adding a new model) automatically. Complex changes need explicit schemas:

swift
enum TodoSchemaV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1, 0, 0)
    static var models: [any PersistentModel.Type] { [Todo.self] }
}

enum TodoSchemaV2: VersionedSchema { ... }

enum TodoMigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [TodoSchemaV1.self, TodoSchemaV2.self]
    }
    static var stages: [MigrationStage] {
        [.lightweight(fromVersion: TodoSchemaV1.self, toVersion: TodoSchemaV2.self)]
    }
}

A warning for solo developers: get your model structure right before you ship. Losing user data is the hardest mistake to recover from. Test the full upgrade path through TestFlight before every release.


12. Other persistence and sync options

12.1 Choosing an approach

NeedApproach
Settings, switches, last selection@AppStorage / UserDefaults
Structured business data you need to querySwiftData
Cross-device sync (same Apple Account)SwiftData + CloudKit, or NSUbiquitousKeyValueStore
Data shared between users, server-side logicCloudKit (public database) or your own backend
Passwords, tokens, secretsKeychain
Files (images, documents, exports)FileManager + Documents directory
Throwaway cacheURLCache / Caches directory / memory

12.2 UserDefaults

swift
UserDefaults.standard.set(true, forKey: "hasSeenOnboarding")
let seen = UserDefaults.standard.bool(forKey: "hasSeenOnboarding")

// sharing data with widgets and extensions requires an App Group
let shared = UserDefaults(suiteName: "group.com.yourname.app")

12.3 The file system

swift
let docs = URL.documentsDirectory              // user data, backed up
let caches = URL.cachesDirectory                // cache, the system may purge it
let temp = URL.temporaryDirectory               // scratch

let fileURL = docs.appending(path: "notes.json")
try data.write(to: fileURL)
let loaded = try Data(contentsOf: fileURL)

// JSON coding
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.dateEncodingStrategy = .iso8601
let json = try encoder.encode(todos)

12.4 CloudKit

Apple's managed backend. Extremely valuable for solo developers: no servers to run, no database to operate, generous free tier, and users are signed in automatically via their Apple Account.

Three databases:

  • Private: the user's own data, accessible only to them. This is what SwiftData's iCloud sync uses.
  • Shared: data the user deliberately shares with others.
  • Public: visible to all users (leaderboards, shared content libraries).

The easiest path is SwiftData with cloudKitDatabase: .automatic. For finer control, use CloudKit directly:

swift
import CloudKit

let container = CKContainer.default()
let db = container.publicCloudDatabase

let record = CKRecord(recordType: "Article")
record["title"] = "A title"
try await db.save(record)

let query = CKQuery(recordType: "Article",
                    predicate: NSPredicate(format: "title CONTAINS %@", "Swift"))
let (results, _) = try await db.records(matching: query)

Caveats: CloudKit + SwiftData sync requires every property to have a default or be optional, relationships must be optional, and you can't use @Attribute(.unique). During development, enable the iCloud capability in Xcode and deploy your schema to production in the CloudKit Dashboard.

12.5 Keychain

For passwords, tokens, and keys. Encrypted by the system, and optionally retained after the app is deleted.

swift
import Security

func saveToken(_ token: String) throws {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: "apiToken",
        kSecValueData as String: Data(token.utf8)
    ]
    SecItemDelete(query as CFDictionary)
    let status = SecItemAdd(query as CFDictionary, nil)
    guard status == errSecSuccess else { throw KeychainError.saveFailed }
}

The Keychain's C-style API is painful. In practice people either wrap it themselves or use one very small third-party package.

12.6 Document-based SwiftUI apps

If your app is the open/edit/save kind (text editor, drawing tool, notes), use DocumentGroup:

swift
@main
struct MyDocApp: App {
    var body: some Scene {
        DocumentGroup(newDocument: TextDocument()) { file in
            EditorView(document: file.$document)
        }
    }
}
Substantially expanded in iOS 27
the new Document API adds WritableDocument / ReadableDocument protocols supporting asynchronous, incremental disk I/O with progress reporting through Foundation's Subprogress — large files no longer need to be read into memory at once. DocumentCreationSource plus NewDocumentButton let you declare multiple creation sources (blank, from template, from import). Document apps are a much better experience now.

13. Networking and backends

13.1 URLSession

swift
struct APIClient {
    let baseURL = URL(string: "https://api.example.com")!

    func fetch<T: Decodable>(_ path: String, as type: T.Type) async throws -> T {
        let url = baseURL.appending(path: path)
        let (data, response) = try await URLSession.shared.data(from: url)

        guard let http = response as? HTTPURLResponse else {
            throw APIError.invalidResponse
        }
        guard (200..<300).contains(http.statusCode) else {
            throw APIError.server(code: http.statusCode)
        }

        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        decoder.dateDecodingStrategy = .iso8601
        return try decoder.decode(T.self, from: data)
    }

    func post<Body: Encodable, T: Decodable>(
        _ path: String, body: Body, as type: T.Type
    ) async throws -> T {
        var request = URLRequest(url: baseURL.appending(path: path))
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpBody = try JSONEncoder().encode(body)

        let (data, _) = try await URLSession.shared.data(for: request)
        return try JSONDecoder().decode(T.self, from: data)
    }
}

Other capabilities you'll want:

swift
// download to disk (no memory blow-up)
let (fileURL, _) = try await URLSession.shared.download(from: url)

// upload
let (data, _) = try await URLSession.shared.upload(for: request, from: fileData)

// stream line by line
for try await line in url.lines { print(line) }

// background downloads (continue after the app is backgrounded)
let config = URLSessionConfiguration.background(withIdentifier: "com.app.download")

You don't need Alamofire. URLSession plus async/await is already concise, and one fewer dependency is one less maintenance burden.

13.2 Network status and connectivity

swift
import Network

let monitor = NWPathMonitor()
monitor.pathUpdateHandler = { path in
    print(path.status == .satisfied ? "online" : "offline")
    print(path.isExpensive ? "cellular" : "Wi-Fi")
}
monitor.start(queue: .global())

13.3 Choosing a backend

For a solo developer, ordered by cost:

  1. No backend: everything local plus iCloud sync. Most utility apps fall here — consider it first.
  2. CloudKit: when you need cross-user sharing or public content. Zero ops, included with your developer account.
  3. BaaS (Supabase, Firebase, etc.): when you need a relational database, realtime subscriptions, or third-party auth.
  4. Your own Swift backend: Vapor or Hummingbird. The upside is sharing Swift type definitions across client and server.
From WWDC26: the Foundation Models framework will be open sourced, meaning the same Swift AI code you write in your app could run on your server. Apple also rewrote the QUIC layer of its networking stack in Swift and open sourced it (swift-nio-quic). Server-side Swift keeps getting stronger.

13.4 Push notifications

swift
import UserNotifications

// request permission
let granted = try await UNUserNotificationCenter.current()
    .requestAuthorization(options: [.alert, .badge, .sound])

// local notification
let content = UNMutableNotificationContent()
content.title = "Time to drink water"
content.body = "It's been two hours"
content.sound = .default

let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 7200, repeats: true)
let request = UNNotificationRequest(identifier: UUID().uuidString,
                                    content: content, trigger: trigger)
try await UNUserNotificationCenter.current().add(request)

Remote push (APNs) requires a server. A lightweight alternative: use CloudKit subscriptions (CKSubscription) to push when data changes, with no server of your own at all.


Part V · Intelligence

14. Foundation Models

This is currently the framework most worth a solo developer's attention. It provides a native Swift API to an on-device large language model — no API key, no token cost, and data never leaves the device.

14.1 Basic usage

swift
import FoundationModels

let session = LanguageModelSession()
let response = try await session.respond(to: "Summarize this in one sentence: \(text)")
print(response.content)

A session with instructions (the equivalent of a system prompt):

swift
let session = LanguageModelSession(
    instructions: """
    You are an expense-tracking assistant. The user will describe a purchase,
    and you extract the amount, category, and merchant. Keep answers brief.
    """
)

Streaming output:

swift
for try await partial in session.streamResponse(to: prompt) {
    displayText = partial.content     // update the UI token by token
}

14.2 Structured output: @Generable

Have the model return a type-safe Swift struct instead of a string you have to parse. This is the best part of the framework.

swift
@Generable
struct Expense {
    @Guide(description: "The amount spent, in dollars")
    let amount: Double

    @Guide(description: "Spending category", .anyOf(["Food", "Transit", "Shopping", "Entertainment", "Other"]))
    let category: String

    @Guide(description: "Merchant name; leave empty if unclear")
    let merchant: String

    @Guide(description: "3-5 tags describing this expense", .count(3...5))
    let tags: [String]
}

let session = LanguageModelSession()
let expense = try await session.respond(
    to: "Spent $5.50 at Starbucks on coffee today",
    generating: Expense.self
).content

print(expense.amount)     // 5.5
print(expense.category)   // "Food"

The @Guide constraints compile into hard constraints on the model's decoding process — the model cannot emit output that doesn't match the type. Far more reliable than "ask GPT for JSON and pray the format is right."

14.3 Tool calling

Let the model call functions in your app:

swift
struct SearchNotesTool: Tool {
    let name = "searchNotes"
    let description = "Search the user's notes for content matching a keyword"

    @Generable
    struct Arguments {
        @Guide(description: "The search keyword")
        let query: String
    }

    func call(arguments: Arguments) async throws -> String {
        let results = await NoteStore.shared.search(arguments.query)
        return results.map(\.title).joined(separator: "\n")
    }
}

let session = LanguageModelSession(
    tools: [SearchNotesTool()],
    instructions: "You can search the user's notes to answer questions."
)
let answer = try await session.respond(to: "What did I write in my design notes last week?")

The model decides when to call the tool and folds the result into its answer.

14.4 The major WWDC26 updates

① Any model provider. A new LanguageModel protocol means LanguageModelSession can now be backed by a local or cloud model:

  • Apple's on-device model (the default)
  • The next-generation Apple model on Private Cloud Compute
  • Cloud models like Claude and Gemini (Anthropic and Google have both published Swift packages)
  • CoreAILanguageModel and MLXLanguageModel (open-source implementations for running your own models)

The point: one codebase, and switching models is a one-line change. Develop against a large cloud model to validate quality, then downgrade to the on-device model to control cost — or choose dynamically based on device capability.

② A free Private Cloud Compute tier. Apple's stated terms: enroll in the App Store Small Business Program and have fewer than 2 million total first-time downloads, and you can use the next-generation Apple Foundation Models on PCC at no cloud API cost. That's a real subsidy for indie developers.

That said, the subject of the eligibility test (per app, or all apps under a developer account combined) is worded slightly differently across Apple's pages. If you're near the threshold, go by the text on the Private Cloud Compute page.

③ Multimodal input. You can pass images alongside text in a prompt, and the model can reason about visual content. Vision framework tools like OCR and barcode reading are available for the model to call directly, all on device.

④ Dynamic Profiles. Swap models, tools, and instructions on the fly within a continuous session, so your app's behavior adapts to context.

⑤ The Evaluations framework. Purpose-built for verifying AI feature reliability — conventional unit tests can't cover "model output quality." Evaluations offers a systematic approach, including hill-climbing iteration on prompts. Instruments support is included, so you can profile agent behavior.

⑥ An `fm` CLI and Python SDK. The same capabilities from the command line and from Python, convenient for data-processing scripts.

14.5 Practical advice

  • First ask whether the feature really needs an LLM. Plenty of requirements are solved faster and more reliably by a regex, the NaturalLanguage framework, or a dropdown menu.
  • The on-device model is not large. It's good at summarizing, classifying, rewriting, extracting, and short conversation. It's bad at complex reasoning, long-form writing, precise arithmetic, and obscure facts.
  • Check availability: SystemLanguageModel.default.availability — older devices, or devices with Apple Intelligence turned off, need a fallback.
  • Use @Generable rather than asking for JSON and parsing it.
  • Give users control over AI output: make it editable, undoable, and clearly labeled as AI-generated.

15. App Intents and system integration

App Intents make your app's functionality callable by the system — Siri, Shortcuts, Spotlight, widgets, Control Center, and the Action button all run through it.

Why it matters for solo developers: this is the most effective way to embed your app in the system and separate yourself from apps that are just an icon, and it isn't expensive to adopt.

15.1 Defining an intent

swift
import AppIntents

struct AddTodoIntent: AppIntent {
    static let title: LocalizedStringResource = "Add to-do"
    static let description = IntentDescription("Add an item to the to-do list")

    @Parameter(title: "Content")
    var text: String

    func perform() async throws -> some IntentResult & ProvidesDialog {
        await TodoStore.shared.add(text)
        return .result(dialog: "Added: \(text)")
    }
}

15.2 Exposing it to Siri

swift
struct MyAppShortcuts: AppShortcutsProvider {
    static var appShortcuts: [AppShortcut] {
        AppShortcut(
            intent: AddTodoIntent(),
            phrases: [
                "Add a to-do in \(.applicationName)",
                "Note something with \(.applicationName)"
            ],
            shortTitle: "Add to-do",
            systemImageName: "plus.circle"
        )
    }
}

15.3 App Entityletting the system understand your data

swift
struct TodoEntity: AppEntity {
    let id: UUID
    let title: String

    static let typeDisplayRepresentation: TypeDisplayRepresentation = "To-do"
    var displayRepresentation: DisplayRepresentation { .init(title: "\(title)") }

    static let defaultQuery = TodoQuery()
}

struct TodoQuery: EntityQuery {
    func entities(for ids: [UUID]) async throws -> [TodoEntity] {
        await TodoStore.shared.todos(ids: ids).map(TodoEntity.init)
    }
    func suggestedEntities() async throws -> [TodoEntity] {
        await TodoStore.shared.recent().map(TodoEntity.init)
    }
}

15.4 What changed at WWDC26

① App Schemas. Apple predefined a set of standard entity schemas and intent schemas. Map your data onto them and you automatically get:

  • Your content in Spotlight's semantic index, so Siri can find it with attribution back to your app
  • Users acting on that content through arbitrary natural language — you don't predefine phrases, and you don't change code as Siri's language understanding improves or expands to new languages

This is a meaningful shift: from "I tell the system which phrasings I support" to "I tell the system what data and capabilities I have, and it handles the rest."

② The View Annotations API. Map views to entities so people can issue natural-language commands about what's on screen ("add this to favorites").

③ The AppIntentsTesting framework. Validate your integration through real system pathways, without UI automation. Previously the worst part of App Intents was not knowing whether you'd wired it up correctly; now it's testable.

CapabilityFramework / API
Spotlight indexingCoreSpotlight; LLM semantic search as of WWDC26
Universal links / deep linksAssociated Domains + .onOpenURL
Sharing into your appShare Extension
Sharing out of your appShareLink / Transferable
Drag and drop.draggable / .dropDestination
Background workBackgroundTasks
Shortcuts automationApp Intents (free)
Control Center controlsControlWidget (see 18.4)
Action button / Camera ControlApp Intents (free)

16. Core AI, Core ML, and the perception frameworks

16.1 Core AI (new at WWDC26)

Core AI is a framework built into the OS and purpose-built for Apple Silicon, for running your own models. The division of labor: Foundation Models solves "use Apple's model"; Core AI solves "use your model."

Characteristics:

  • A modern, memory-safe Swift API for loading, specializing, and running models entirely on device
  • Models are specialized automatically for the hardware they run on, with ahead-of-time compilation for fast loading
  • Fine-grained inference memory control, zero-copy data paths, stateful execution
  • Covers everything from compact vision models to large-scale generative AI
  • Ships CoreAILanguageModel, which plugs directly into the Foundation Models framework as a model backend
  • Reuses familiar Python / PyTorch workflows for model authoring, optimization, and conversion

Zero server dependency, zero token cost. If you want to run models you trained yourself locally, this is the new main road.

16.2 Core ML

Still the general-purpose framework for deploying machine learning models. Suited to classification, detection, style transfer, and other traditional ML tasks.

swift
let model = try MyImageClassifier(configuration: MLModelConfiguration())
let output = try await model.prediction(image: pixelBuffer)
print(output.classLabel, output.classLabelProbs)

Its companion, Create ML, is a graphical training tool (Xcode → Open Developer Tool → Create ML) that trains image classification, object detection, sound classification, action classification, tabular regression, and more without writing code. The barrier for a solo developer is very low.

16.3 MLX

Apple's open-source array and training framework for Apple Silicon, used to experiment with, train, and fine-tune large models. WWDC26 added Metal 4 and GPU Neural Accelerator support, plus distributed training across multiple Macs over RDMA on Thunderbolt.

If you're just using models, reach for Foundation Models or Core AI. If you're modifying models, reach for MLX.

16.4 Visionimage understanding

swift
import Vision

// text recognition (OCR)
let request = RecognizeTextRequest()
let results = try await request.perform(on: image)
for observation in results {
    print(observation.topCandidates(1).first?.string ?? "")
}

What Vision provides: text recognition, barcode reading, face detection and landmarks, body pose, hand pose, object trajectories, saliency analysis (smart cropping), image similarity, and document scanning. All on device.

As of WWDC26, Vision's OCR and barcode reading can be exposed directly as tools for Foundation Models to call. watchOS 27 supports Vision for the first time.

16.5 Other perception and language frameworks

FrameworkCapability
Speechspeech to text, with on-device recognition
AVSpeechSynthesizertext to speech
Translationsystem-level translation, with UI or as a pure API
NaturalLanguagetokenization, POS tagging, named entity recognition, language identification, sentiment analysis, word embeddings
SoundAnalysissound event classification
Music Understandingnew at WWDC26, on-device audio analysis across six dimensions
VisionKitready-made UI: document scanning, Live Text, Visual Look Up

NaturalLanguage deserves a special mention: many requirements that "need AI" (keyword extraction, language detection, coarse sentiment) are fully served by it, orders of magnitude faster and completely deterministic.


Part VI · System capabilities

17. The system capability landscape

This chapter is a directory. Don't read it straight through — come here when you need a specific capability.

17.1 Location and maps

swift
import MapKit
import CoreLocation

// maps (SwiftUI-native)
Map {
    Marker("Office", coordinate: officeCoordinate)
    Annotation("Home", coordinate: homeCoordinate) {
        Image(systemName: "house.fill")
    }
    MapPolyline(coordinates: route)
        .stroke(.blue, lineWidth: 4)
}
.mapStyle(.standard(elevation: .realistic))
.mapControls { MapUserLocationButton(); MapCompass() }

// location
let manager = CLLocationManager()
manager.requestWhenInUseAuthorization()

// place search
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = "coffee"
let response = try await MKLocalSearch(request: request).start()

Related: MapKit (maps and search), CoreLocation (location, geofencing, beacons), Contacts (address book).

17.2 Health and fitness

swift
import HealthKit

let store = HKHealthStore()
try await store.requestAuthorization(toShare: [], read: [
    HKQuantityType(.stepCount), HKQuantityType(.heartRate)
])

let descriptor = HKSampleQueryDescriptor(
    predicates: [.quantitySample(type: HKQuantityType(.stepCount))],
    sortDescriptors: [SortDescriptor(\.startDate, order: .reverse)],
    limit: 100
)
let samples = try await descriptor.result(for: store)

New at WWDC26 (both from Apple's WWDC26 watchOS guide):

Related: HealthKit, WorkoutKit (build workout plans and push them to Apple Watch), CoreMotion (accelerometer, gyroscope, step counting).

17.3 Weather

swift
import WeatherKit

let weather = try await WeatherService.shared.weather(for: location)
print(weather.currentWeather.temperature)
print(weather.dailyForecast.forecast.first?.highTemperature ?? "")

500,000 calls a month are included with your developer account; beyond that it's paid. Note you must display the Apple Weather attribution and link as required.

17.4 Camera, photos, and media

NeedFramework
Let the user pick photosPhotosPicker (SwiftUI-native, no photo library permission needed)
Read/write the photo libraryPhotoKit
Custom cameraAVFoundation (AVCaptureSession)
System camera capture UIUIImagePickerController bridge (or build your own with AVCaptureSession)
Generative imagesImagePlayground (Apple Intelligence — not a camera or picker)
Video playbackVideoPlayer (SwiftUI) / AVKit
Audio playback and recordingAVFoundation / AVAudioEngine
Image processing and filtersCore Image
Control Center playback integration`NowPlaying` (new at WWDC26)
swift
// picking photos
@State private var item: PhotosPickerItem?
PhotosPicker("Choose a photo", selection: $item, matching: .images)
    .onChange(of: item) { _, new in
        Task {
            if let data = try? await new?.loadTransferable(type: Data.self) {
                image = UIImage(data: data)
            }
        }
    }

// video playback
VideoPlayer(player: AVPlayer(url: videoURL))

WWDC26 updates: the NowPlaying framework unifies playback state across the Lock Screen, Control Center, Dynamic Island, and CarPlay; Core Image's RAW processing reached version 9 with noticeably better sharpness and color; generated subtitles and subtitle styles were added.

17.5 Music

  • MusicKit — the Apple Music catalog, playlists, and user library
  • ShazamKit — music recognition, and matching against your own audio fingerprint catalog
  • Music Understanding (new at WWDC26) — on-device six-dimension audio analysis

17.6 Payments and wallet

  • StoreKit — in-app purchases and subscriptions (chapter 22)
  • PassKit — Apple Pay, Wallet passes
  • Wallet extensions — membership cards, tickets, access keys

17.7 Authentication and security

swift
// Sign in with Apple
SignInWithAppleButton(.signIn) { request in
    request.requestedScopes = [.fullName, .email]
} onCompletion: { result in
    // handle the result
}
.signInWithAppleButtonStyle(.black)

// biometrics
import LocalAuthentication
let context = LAContext()
let ok = try await context.evaluatePolicy(
    .deviceOwnerAuthenticationWithBiometrics,
    localizedReason: "Unlock your notes"
)

Related: AuthenticationServices (Sign in with Apple, passkeys, password autofill), LocalAuthentication (Face ID / Touch ID), CryptoKit (encryption, signing, hashing).

Prefer passkeys: passwordless sign-in, better on both security and experience, and well supported across the Apple ecosystem.

17.8 Privacy and parental controls

Apple tightens this area every year, and solo developers need to keep up:

Framework / requirementNotes
Privacy ManifestYou must declare the data types your app and its SDKs collect, and the reasons for certain API use
AppTrackingTransparencyCross-app tracking requires an explicit permission prompt
DeclaredAgeRangeGet an age range from the system (without the exact birthday) to serve age-appropriate content
PermissionKitA unified flow for children requesting parental approval
FamilyControls / ManagedSettings / DeviceActivityThe official frameworks for Screen Time-style apps
Time AllowancesNew in iOS 27. The system offers parents category-based time management (Entertainment, Games, Social Media). Starting September 2026, you must indicate in the age rating questionnaire whether your app has social media capabilities in order to submit new versions or updates.

Required action for solo developers: determine whether your app meets the "social media capabilities" definition (letting user content spread to many users through a feed or similar discovery mechanism). If so, it goes into the Social Media time allowance category and receives a minimum 13+ rating.

17.9 Devices and connectivity

NeedFramework
Bluetooth peripheralsCoreBluetooth
NFC tagsCoreNFC
Network state and custom protocolsNetwork
Local network discoveryNetwork (Bonjour)
Accessory communicationExternalAccessory / AccessorySetupKit
Smart homeHomeKit / Matter
HapticsCoreHaptics / .sensoryFeedback()
Screen mirroring and castingAVRoutePickerView / AirPlay

17.10 System services

NeedFramework
Local and remote notificationsUserNotifications
Background workBackgroundTasks
Large asset delivery`BackgroundAssets` (self-hosted, or Apple-Hosted with 200 GB of hosting per app included with the developer account)
Calendar and remindersEventKit
ContactsContacts
File selection.fileImporter / .fileExporter (SwiftUI-native)
Watch/play togetherGroupActivities (SharePlay)
In-app rating promptStoreKit's requestReview
ClipboardUIPasteboard / PasteButton (SwiftUI)
Important change
On-Demand Resources are formally deprecated starting in iOS 27 / iPadOS 27 / tvOS 27 / visionOS 27. Migrate to Apple-Hosted Background Assets. Apple has been promoting this migration path since WWDC25 / iOS 26; iOS 27 is where the deprecation lands, not the first announcement. Existing apps keep working in the near term, but plan the migration now.

18. Widgets, Live Activities, and Controls

What these frameworks have in common: your app appears outside your app. For solo developers this is a low-cost, high-visibility differentiator.

18.1 WidgetKit basics

swift
import WidgetKit
import SwiftUI

struct TodoEntry: TimelineEntry {
    let date: Date
    let pendingCount: Int
}

struct TodoProvider: TimelineProvider {
    func placeholder(in context: Context) -> TodoEntry {
        TodoEntry(date: .now, pendingCount: 3)
    }
    func getSnapshot(in context: Context, completion: @escaping (TodoEntry) -> Void) {
        completion(TodoEntry(date: .now, pendingCount: currentCount()))
    }
    func getTimeline(in context: Context, completion: @escaping (Timeline<TodoEntry>) -> Void) {
        let entry = TodoEntry(date: .now, pendingCount: currentCount())
        completion(Timeline(entries: [entry], policy: .after(.now.addingTimeInterval(3600))))
    }
}

struct TodoWidgetView: View {
    let entry: TodoEntry
    var body: some View {
        VStack {
            Text("\(entry.pendingCount)").font(.largeTitle.bold())
            Text("To-do").font(.caption)
        }
        .containerBackground(.fill.tertiary, for: .widget)
    }
}

@main
struct TodoWidget: Widget {
    var body: some WidgetConfiguration {
        StaticConfiguration(kind: "TodoWidget", provider: TodoProvider()) { entry in
            TodoWidgetView(entry: entry)
        }
        .configurationDisplayName("To-do count")
        .supportedFamilies([.systemSmall, .systemMedium, .accessoryCircular])
    }
}

Key points:

  • A widget is a separate extension target, sharing data with the main app through an App Group
  • Widget views are static snapshots — no live interactive animation
  • Interaction happens only through App Intents (buttons, toggles)
  • Timeline updates have a system budget; you cannot refresh at high frequency

Where they appear: Home Screen, Lock Screen, StandBy, the Mac desktop and Notification Center, and Apple Watch complications.

New in iOS 27
widgets can be customized through App Intents and support dynamic styling.

18.2 Interactive widgets

swift
struct ToggleTodoIntent: AppIntent {
    static let title: LocalizedStringResource = "Toggle completion"
    @Parameter var todoID: String

    func perform() async throws -> some IntentResult {
        await TodoStore.shared.toggle(id: todoID)
        return .result()
    }
}

// in the widget view
Button(intent: ToggleTodoIntent(todoID: todo.id)) {
    Image(systemName: todo.isDone ? "checkmark.circle.fill" : "circle")
}

18.3 Live Activities

Real-time status in the Dynamic Island and on the Lock Screen: food delivery, game scores, timers, rides.

swift
import ActivityKit

struct DeliveryAttributes: ActivityAttributes {
    struct ContentState: Codable, Hashable {
        var status: String
        var estimatedMinutes: Int
    }
    let orderNumber: String
}

// start
let activity = try Activity.request(
    attributes: DeliveryAttributes(orderNumber: "12345"),
    content: .init(state: .init(status: "Out for delivery", estimatedMinutes: 20), staleDate: nil)
)

// update
await activity.update(.init(state: .init(status: "Arriving soon", estimatedMinutes: 3), staleDate: nil))

// end
await activity.end(nil, dismissalPolicy: .immediate)

Define the UI with ActivityConfiguration, covering the Lock Screen view and the Dynamic Island's compact and expanded states. Updates can be pushed remotely.

18.4 Controls (Control Center)

Introduced in iOS 18. Your functionality can appear in Control Center, on the Lock Screen, and on the Action button.

swift
struct QuickAddControl: ControlWidget {
    var body: some ControlWidgetConfiguration {
        StaticControlConfiguration(kind: "QuickAdd") {
            ControlWidgetButton(action: AddTodoIntent()) {
                Label("Quick add", systemImage: "plus")
            }
        }
    }
}

ControlWidgetToggle is also available for on/off controls.


Part VII · Graphics and space

19. Graphics, motion, and games

19.1 Drawing in SwiftUI

Most custom graphics never need to leave SwiftUI:

swift
// shapes
Circle()
RoundedRectangle(cornerRadius: 12)
Capsule()

// custom shapes
struct Triangle: Shape {
    func path(in rect: CGRect) -> Path {
        var p = Path()
        p.move(to: CGPoint(x: rect.midX, y: rect.minY))
        p.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
        p.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
        p.closeSubpath()
        return p
    }
}

// Canvas — imperative, high-performance drawing (use it for lots of primitives)
Canvas { context, size in
    for i in 0..<1000 {
        context.fill(Path(ellipseIn: randomRect(in: size)), with: .color(.blue))
    }
}

// gradients and effects
.background(LinearGradient(colors: [.blue, .purple], startPoint: .top, endPoint: .bottom))
MeshGradient(width: 3, height: 3, points: [...], colors: [...])
.blur(radius: 8)
.visualEffect { content, proxy in
    content.scaleEffect(proxy.frame(in: .global).minY / 1000)
}

19.2 Metal shaders

SwiftUI can call Metal shaders directly for custom visual effects:

swift
// Shaders.metal
[[ stitchable ]] half4 wave(float2 position, half4 color, float time) {
    // ...
}

// SwiftUI
Image("photo")
    .colorEffect(ShaderLibrary.wave(.float(time)))

Three entry points: colorEffect (change color), distortionEffect (change position), layerEffect (read surrounding pixels).

New in iOS 27
the session "Compose advanced graphics effects with SwiftUI" covers stronger effect-composition capabilities alongside the refreshed material system.

19.3 2D and 3D

NeedRecommendation
2D games / particle effectsSpriteKit
3D content (new projects)`RealityKit`
3D content (legacy projects)SceneKit (deprecated; don't use for new work)
Low-level graphics / custom render pipelinesMetal
Game Center, achievements, leaderboardsGameKit
Controller supportGameController
Spatial audioPHASE

WWDC26 game updates: Game Porting Toolkit 4 adds open-source agentic coding skills that bring Metal and Apple platform best practices into every step of the porting process; a Steam Asset Converter was added; Unity now has an official StoreKit plug-in.


20. RealityKit and visionOS

20.1 What RealityKit is

Apple's modern 3D engine, spanning visionOS, iOS, and macOS. It uses an ECS (entity-component-system) architecture:

  • Entity: a thing in the scene
  • Component: data attached to an entity (model, collision, physics, audio…)
  • System: per-frame processing of entities with particular components
swift
import RealityKit

struct ImmersiveView: View {
    var body: some View {
        RealityView { content in
            // load a model
            if let robot = try? await Entity(named: "Robot") {
                robot.position = [0, 1, -2]
                robot.components.set(InputTargetComponent())
                robot.generateCollisionShapes(recursive: true)
                content.add(robot)
            }

            // create procedurally
            let sphere = ModelEntity(
                mesh: .generateSphere(radius: 0.1),
                materials: [SimpleMaterial(color: .blue, isMetallic: true)]
            )
            content.add(sphere)
        } update: { content in
            // respond to state changes
        }
        .gesture(
            TapGesture().targetedToAnyEntity().onEnded { value in
                value.entity.position.y += 0.1
            }
        )
    }
}

20.2 The three visionOS presentation modes

swift
@main
struct SpatialApp: App {
    var body: some Scene {
        // 1. Window — flat UI, similar to an iPad app
        WindowGroup { ContentView() }

        // 2. Volume — a 3D box with depth, coexisting with other apps in the Shared Space
        WindowGroup(id: "volume") {
            ModelView()
        }
        .windowStyle(.volumetric)
        .defaultSize(width: 0.5, height: 0.5, depth: 0.5, in: .meters)

        // 3. Immersive Space — takes over the whole environment
        ImmersiveSpace(id: "immersive") {
            ImmersiveView()
        }
        .immersionStyle(selection: $style, in: .mixed, .progressive, .full)
    }
}

Advice for solo developers: start by compiling your iPad app for visionOS (it often runs unchanged), validate whether there's value, then gradually add volumes and immersive content. Don't start with a fully immersive experience.

20.3 What's new in visionOS 27 / RealityKit

  • Physical space lighting: virtual light sources can illuminate real-world surfaces
  • Projective textures: add textures to spotlights, simulating stained glass projections or underwater caustics
  • Real-time cloth simulation: flags, curtains, and clothing respond naturally to movement and interaction
  • Reverb Mesh API: model sound absorption and scattering by environment material for realistic spatial audio
  • 3D Gaussian splats: efficiently render photorealistic scans of real objects
  • Reality Composer Pro 3: the Mac 3D authoring tool, deeply integrated with Xcode, with visual scripting, generative intelligence for asset creation, and Live Preview on Vision Pro (change a material or animation and see it on device in seconds)
  • Spatial Preview framework: Mac apps can push spatial photos, Apple Immersive Video, and 3D content straight to Quick Look on Vision Pro, with live USD editing and SharePlay collaboration
  • Enhanced object tracking: high-frame-rate tracking, an extended training option in Create ML, and a metric-space pose API. Reference objects work across iOS and visionOS without retraining (iOS gets equivalent capability through ARKit)
  • Foveated Streaming framework (introduced in visionOS 26.4): stream high-quality content only where the user is looking
  • Spatial accessories: third parties can build 6DoF-tracked accessories combining IR LEDs and an IMU, tracked at up to 90 Hz

20.4 ARKit

For augmented reality on iOS, or for environment understanding on visionOS (planes, scene mesh, hands, world tracking). In SwiftUI it's typically combined with RealityKit.


Part VIII · Platforms and delivery

21. Platform differences at a glance

One SwiftUI codebase runs everywhere, but the experience design must differ by platform.

PlatformCore interactionUnique capabilitiesDesign notes
iOStouch, one-handedcamera, sensors, Live Activities, Dynamic Islandsingle-column layouts, large tap targets, actions near the bottom
iPadOStouch + keyboard/mouse + Apple Pencilmultiple windows, Slide Over, external displays, PencilKitmulti-column layouts, keyboard shortcuts, drag and drop
macOSkeyboard and mousemenu bar, multiple windows, Settings scene, command line, background residencyhigh information density, context menus and shortcuts
watchOSDigital Crown + touch, very short sessionscomplications, workout sessions, notification forwarding, WorkoutKitone thing per screen, large type, complications as the entry point
tvOSremote-based focus navigationTVUIKit, Top Shelffocus-driven, 10-foot viewing distance, avoid text entry
visionOSeye tracking + hand gesturesimmersive spaces, spatial audio, object trackingrespect the user's physical environment, avoid blocking their view

Organizing cross-platform code:

swift
#if os(iOS)
    .navigationBarTitleDisplayMode(.inline)
#elseif os(macOS)
    .frame(minWidth: 600, minHeight: 400)
#endif

// better: branch on size class rather than platform
@Environment(\.horizontalSizeClass) private var sizeClass
if sizeClass == .compact { VStack { ... } } else { HStack { ... } }

macOS-specific scenes:

swift
@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup { ContentView() }
            .commands {                       // menu bar
                CommandGroup(after: .newItem) {
                    Button("Import…") { }.keyboardShortcut("i", modifiers: .command)
                }
            }

        Settings { SettingsView() }           // preferences window

        MenuBarExtra("Status", systemImage: "star") {   // menu bar item
            StatusView()
        }
    }
}

About Intel: there are two independent threads here; don't conflate them.

  • Architecture support: macOS 26 Tahoe is the last release line supporting Intel Macs; macOS 27 (Golden Gate) is Apple Silicon only, and Xcode 27 itself is arm64 only.
  • App Store policy: Apple stated at WWDC26 that apps and games offered as Universal Purchases on the Mac App Store no longer need to support Intel, provided "your app or game supports macOS 13.0 or later" (Apple's own precondition, defining when you can drop Intel support in App Store Connect).

The practical upshot: as a solo developer you can now ship Apple Silicon only and stop maintaining universal binaries.


22. StoreKit and monetization

22.1 StoreKit 2

Rewritten around async/await and far more concise than the first generation. Use StoreKit 2 for new projects.

swift
import StoreKit

@MainActor
@Observable
final class Store {
    private(set) var products: [Product] = []
    private(set) var purchasedIDs: Set<String> = []

    private let productIDs = ["com.app.pro.monthly", "com.app.pro.yearly", "com.app.lifetime"]
    private var updateTask: Task<Void, Never>?

    init() {
        updateTask = Task { await observeTransactions() }
    }

    func loadProducts() async {
        products = (try? await Product.products(for: productIDs)) ?? []
    }

    func purchase(_ product: Product) async throws {
        let result = try await product.purchase()
        switch result {
        case .success(let verification):
            let transaction = try checkVerified(verification)
            await updateEntitlements()
            await transaction.finish()
        case .userCancelled, .pending:
            break
        @unknown default:
            break
        }
    }

    func restore() async throws {
        try await AppStore.sync()
        await updateEntitlements()
    }

    private func updateEntitlements() async {
        var ids: Set<String> = []
        for await result in Transaction.currentEntitlements {
            if let t = try? checkVerified(result) {
                ids.insert(t.productID)
            }
        }
        purchasedIDs = ids
    }

    // watch for transactions that happen elsewhere (Family Sharing, refunds, renewals)
    private func observeTransactions() async {
        for await result in Transaction.updates {
            if let t = try? checkVerified(result) {
                await updateEntitlements()
                await t.finish()
            }
        }
    }

    private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
        switch result {
        case .verified(let safe): return safe
        case .unverified: throw StoreError.failedVerification
        }
    }
}

enum StoreError: Error { case failedVerification }

22.2 Ready-made SwiftUI store UI

If you don't want to design a paywall, use the system components:

swift
// subscription page (Apple's standard layout, localized, handles purchase flow)
SubscriptionStoreView(groupID: "YOUR_GROUP_ID") {
    VStack {
        Image(systemName: "star.circle.fill").font(.system(size: 60))
        Text("Upgrade to Pro").font(.largeTitle.bold())
    }
}
.storeButton(.visible, for: .restorePurchases)
.subscriptionStorePolicyDestination(url: privacyURL, for: .privacyPolicy)

// a single product
ProductView(id: "com.app.lifetime")

// a product list
StoreView(ids: productIDs)

22.3 Testing in-app purchases

Use a StoreKit Configuration file to test locally without hitting the App Store:

  1. File → New → File → StoreKit Configuration File
  2. Configure products, prices, and subscription groups in it
  3. Scheme → Edit Scheme → Run → Options → select it under StoreKit Configuration
  4. You can simulate successful purchases, failures, accelerated renewals, and refunds

For real testing, use Sandbox accounts (created in App Store Connect) plus TestFlight.

22.4 New subscription options from WWDC26

The ones most relevant to solo developers:

CapabilityWhat it isTiming
Retention MessagingShow a message and a special offer when a subscriber taps cancel, without adding friction to the cancellation flow. Configurable in App Store Connect, with an API for real-time interactionThis fall
Subscription Bundles / SuitesBundle: buy several existing subscriptions in one purchase. Suite: a set of subscriptions not sold individually, packaged as oneDetails this summer
Group PurchasesA subscriber buys multiple seats and invites others; Apple provides the invitation flowLater this year
Volume PurchasingSell to enterprise and education buyers through Apple School Manager and Apple Business ManagerThis fall
Monthly with a 12-month commitmentA cheaper monthly rate in exchange for a one-year commitment; users can see completed and remaining payments in their Apple AccountAvailable now (requires iOS 26.4+, excluding the US and Singapore)
Unified in-app purchase submissionGroup multiple IAPs, subscriptions, events, and custom product pages into one submissionThis summer

22.5 Pricing strategy (from a solo developer's perspective)

This lays out common approaches and trade-offs; the right choice depends on your product:

  • One-time purchase: high user acceptance, no recurring revenue. Suits utilities with a clear functional boundary.
  • Subscription: recurring revenue supports long-term maintenance, but you must keep delivering value or churn will be high. Suits apps with server costs, continuously updated content, or continuously growing functionality.
  • Free with in-app unlock: the shortest conversion path. The key is that the free tier must genuinely be useful, not a crippled demo.
  • Paid download: low conversion unless you have strong word of mouth or press coverage.

The App Store Small Business Program: developers earning under $1M a year pay 15% commission instead of 30%. You have to apply — it isn't automatic. As of WWDC26, apps in the program with fewer than 2 million total first-time downloads also get free access to Apple's Foundation Models on Private Cloud Compute.


23. Testing, debugging, and shipping

23.1 Swift Testing

Apple's new official unit testing framework, and the recommended choice for new projects, with much cleaner syntax than XCTest. But XCTest hasn't gone anywhere: UI tests still require XCUITest, and XCTest in legacy projects keeps running — as of Swift 6.4 the two interoperate and can coexist in the same test target for gradual migration.

swift
import Testing
@testable import MyApp

@Test func addingATodoIncreasesTheCount() {
    let store = TodoStore()
    store.add("Buy milk")
    #expect(store.todos.count == 1)
    #expect(store.todos.first?.title == "Buy milk")
}

@Test("Empty titles should be rejected")
func rejectsEmptyTitle() throws {
    let store = TodoStore()
    #expect(throws: ValidationError.self) {
        try store.addValidated("")
    }
}

// parameterized tests: one test, many inputs
@Test(arguments: [("", false), ("a", true), (String(repeating: "x", count: 200), false)])
func titleValidation(input: String, expected: Bool) {
    #expect(Todo.isValidTitle(input) == expected)
}

// suites and tags
@Suite("To-do storage")
struct TodoStoreTests {
    @Test(.tags(.critical)) func persistence() async throws { ... }
}

When #expect fails it shows the actual value of every sub-expression, which is far more useful than XCTAssert's output.

23.2 Debugging

  • Breakpoints: click the line number. Right-click to add conditions or log without pausing.
  • `print` and `dump`: dump(object) prints the full structure.
  • View hierarchy debugging: click the Debug View Hierarchy icon while running to explode the UI in 3D.
  • Instruments (⌘I): performance profiling. Common templates: Time Profiler (CPU), Allocations (memory), Leaks, SwiftUI (view redraw counts), Animation Hitches (dropped frames).
  • As of WWDC26, Instruments can profile Foundation Models agent behavior.
  • The `.background(.red)` trick: when layout misbehaves, give the suspicious view a colored background and you'll immediately see how much space it takes.
  • `Self._printChanges()`: put it in body to print what triggered the redraw. The best tool for diagnosing SwiftUI performance problems.

23.3 What's new in Xcode 27

  • Coding agents: pick your model, and use the official SwiftUI agent skills to steer it toward this year's best practices
  • Device Hub: manage all your devices in one place
  • Neural Engine-powered code completion
  • Faster builds: changes to ViewBuilder (now exposed as ContentBuilder) noticeably improve SwiftUI compile times
  • Localization workflow improvements

23.4 The shipping process

1. Create an app record in App Store Connect (Bundle ID must match Xcode)
2. Prepare assets: icon, screenshots (all sizes), description, keywords, privacy labels, age rating
3. Xcode: Product → Archive → Distribute App → App Store Connect
4. Assign the build to TestFlight in App Store Connect
5. TestFlight beta (up to 100 internal, 10,000 external testers; external requires a light review)
6. Collect feedback, fix, iterate
7. Submit for review (typically 24–48 hours)
8. Release manually or automatically once approved

Practical advice for solo developers:

  • Screenshots are the single biggest factor in conversion — considerably more so than your description text. Invest time here.
  • As of WWDC26, App Store Connect has an Asset Library (a central place to manage all visual assets, submittable independently of an app release) and a product page preview tool (see the real result before publishing). Both arrive this fall.
  • Additional placements were added too: product page headers, search results creative assets, and more.
  • Fill in privacy labels honestly. Getting caught misrepresenting them means removal.
  • Common first-submission rejections: incomplete functionality, placeholder content, crashes, in-app purchases with no restore button, a broken privacy policy link, and requiring login without providing a test account.
  • Provide a demo account for the reviewer, noted in the review notes.
  • Xcode Cloud can automate builds and TestFlight distribution, with some free allowance included in the developer account. For a one-person team, what it saves is the twenty minutes of manual archiving on every release.

Appendices

Appendix A. Framework cheat sheet

Indexed by "what am I trying to do."

Interface

NeedFramework
Cross-platform UISwiftUI
ChartsSwift Charts
Feature discovery tipsTipKit
Widgets / complicationsWidgetKit
Live Activities / Dynamic IslandActivityKit
Control Center controlsWidgetKit (ControlWidget)
Handwriting and drawingPencilKit
Rich text editingSwiftUI TextEditor + AttributedString
Web contentWebKit
Data observationObservation

Data

NeedFramework
Local databaseSwiftData
Settings@AppStorage / UserDefaults
iCloud syncSwiftData + CloudKit
Backend servicesCloudKit
SecretsKeychain (Security)
Files and documentsFileManager / DocumentGroup
NetworkingURLSession / Network

Intelligence

NeedFramework
On-device LLMFoundationModels
Running your own modelsCore AI
Evaluating AI featuresEvaluations
Traditional ML modelsCore ML / Create ML
Model training and fine-tuningMLX
Image understandingVision / VisionKit
Speech recognitionSpeech
TranslationTranslation
Text analysisNaturalLanguage
Audio analysisSoundAnalysis / Music Understanding
System integration and SiriApp Intents
Search indexingCoreSpotlight

System capabilities

NeedFramework
Maps and locationMapKit / CoreLocation
Health and fitnessHealthKit / WorkoutKit / CoreMotion
WeatherWeatherKit
Camera and mediaAVFoundation / PhotoKit / Core Image
Playback integrationNowPlaying
MusicMusicKit / ShazamKit
NotificationsUserNotifications
Background workBackgroundTasks / BackgroundAssets
Calendar and contactsEventKit / Contacts
PaymentsStoreKit / PassKit
Sign-in and securityAuthenticationServices / LocalAuthentication / CryptoKit
Privacy and parental controlsAppTrackingTransparency / DeclaredAgeRange / PermissionKit / FamilyControls
Bluetooth / NFC / accessoriesCoreBluetooth / CoreNFC / AccessorySetupKit
Smart homeHomeKit / Matter
HapticsCoreHaptics
Watch/play togetherGroupActivities

Graphics and space

NeedFramework
3D contentRealityKit + Reality Composer Pro
2D gamesSpriteKit
Low-level graphicsMetal
Environment understanding / ARARKit
Spatial content previewSpatial Preview
Foveated streamingFoveated Streaming
Game servicesGameKit / GameController
Spatial audioPHASE

Engineering

NeedTool
Unit testsSwift Testing
UI testsXCUITest
Dependency managementSwift Package Manager
Documentation generationDocC
Performance profilingInstruments
CI/CDXcode Cloud

Appendix B. Pitfalls

First, what this table is: a learning-path trade-off for beginners, not a claim that the left column is obsolete. Most of the left column still runs fine, still has enormous amounts of existing code, and still backs third-party SDKs. Some situations (UI testing, certain advanced Core Data features, controls SwiftUI hasn't covered) will find you eventually. The point is: as a beginner with limited time, spend it on the right column, and learn the left column when you actually run into it.

Not first, as a beginnerLearn this instead
Objective-CSwift
UIKit / AppKitSwiftUI
Core DataSwiftData
ObservableObject / @Published / @StateObject@Observable + @State
NavigationViewNavigationStack / NavigationSplitView
SceneKitRealityKit
StoreKit 1 (the receipt-validation world)StoreKit 2
XCTest (for unit tests)Swift Testing (UI tests still use XCUITest)
CocoaPods / CarthageSwift Package Manager
DispatchQueue / callback hellasync/await + actors
On-Demand Resources (deprecated in iOS 27)Apple-Hosted Background Assets
WatchKit's UI layerSwiftUI for watchOS
Storyboards / XIBsSwiftUI (with #Preview)
MVVM dogma@State first; extract @Observable when you need it

Of the above, only On-Demand Resources is explicitly marked deprecated by Apple. NavigationView and SceneKit are deprecated or clearly superseded. The rest is "there's a better new option," not "the old thing doesn't work."

Common beginner traps:

  1. Side effects inside `body`. Network requests and database writes belong in .task {}.
  2. Overusing `GeometryReader`. It fills whatever space the parent offers, which frequently wrecks layouts. Try ViewThatFits, .containerRelativeFrame, or Grid first.
  3. Force unwrapping with `!`. Half the crash reports after launch trace back to this.
  4. Ignoring `Sendable` warnings and papering over them with @unchecked. Those warnings are protecting you.
  5. Building elaborate architecture up front. Speed is a solo developer's biggest advantage; don't cancel it out yourself.
  6. Reimplementing system components. The system share sheet, subscription page, and photo picker are already good, and they improve automatically with OS updates.
  7. Skipping localization. Adding an English version to a Chinese app (or vice versa) multiplies your potential market for very little cost — String Catalog plus one round of translation.
  8. Not testing the upgrade path before release. Existing users losing their data on upgrade is the hardest mistake to recover from.

Appendix C. Resources and staying current

Official (highest priority)

Community

A rhythm for staying current

  • June: WWDC. Watch the Keynote and Platforms State of the Union for the big picture, then pick 10–15 sessions matching your stack. The Guides page is organized by topic — don't browse at random.
  • Ongoing: subscribe to the monthly "What's new in Swift" on the Swift.org blog, plus one or two community newsletters.
  • Every major OS release: read the release notes for the frameworks you use, paying particular attention to deprecations.

One last thing

Frameworks change, languages change, the design language gets overhauled every few years. What doesn't change: understanding what people need in a given situation, then doing it well with as little machinery as possible.

This guide gives you the map. You have to walk it yourself, and you have to walk it by building — ten tutorials read is worth less than one small app finished.

Good luck.


Written from publicly available information as of July 2026, aligned with the WWDC26 announcements.

Two reminders: (1) at the time of writing, iOS 27 / macOS 27, Xcode 27, Swift 6.4, and SF Symbols 8 are all in beta, with final releases in the fall and APIs still subject to change; (2) App Store capabilities marked "this fall" or "later this year" are not yet fully available. For anything that will land in code or a business decision, go by the [Apple documentation](https://developer.apple.com/documentation/), the [Xcode system requirements page](https://developer.apple.com/xcode/system-requirements/), and the actual state of App Store Connect.

A Simplified Chinese version of this document is available as `Apple-开发生态完整指南.md`.