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
| Item | Requirement | Notes |
|---|---|---|
| Mac | Apple Silicon (M series) required | Xcode 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 |
| OS | Shipping 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 |
| Xcode | Shipping version from the Mac App Store (currently 26.5, Swift 6.3)<br>Xcode 27 beta from the developer downloads page | Includes the compiler, simulators, Instruments, and previews |
| Developer account | A free account is enough to start | On-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 |
| Hardware | A real device helps a lot | The 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;
⌥⌘Prefreshes it. - Inspector (right, ⌥⌘0): attribute panel.
- Console (bottom, ⇧⌘C):
printoutput 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
printstatements, 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;
@AppStoragehandles 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
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 BoolHabit: 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
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) // tupleString interpolation:
let age = 30
print("My name is \(name) and I'm \(age)")Multi-line strings:
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?.
var nickname: String? = nil // no nickname yet
nickname = "Jer" // now there is oneYou cannot use an optional directly. You have to handle the "doesn't exist" case first. Three ways:
// 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:
let length = nickname?.count // Int?; nil overall when nickname is nilForce 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
// 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:
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
// 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.
// 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, +) // 6SwiftUI's Button is exactly this syntax:
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.
// 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"// 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) // 1When 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).
// 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
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
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 sparinglyMake errors user-friendly:
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:
// 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 warningsThe 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."
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:
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:
| Protocol | What it gives you |
|---|---|
Equatable | == comparison |
Hashable | usable in a Set or as a dictionary key |
Comparable | <, >, sorting |
Identifiable | a unique id; required by SwiftUI's List/ForEach |
Codable | conversion to and from JSON and similar formats |
Sendable | safe to pass across concurrency boundaries |
CaseIterable | iterate all cases of an enum |
In most cases just declaring conformance is enough; the compiler synthesizes the implementation:
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:
extension String {
var isValidEmail: Bool {
contains("@") && contains(".")
}
}
"[email protected]".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:
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`:
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 arraysSwiftUI'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:
| Macro | From | What it does |
|---|---|---|
@Observable | Observation | makes a class's property changes observable by SwiftUI |
@Model | SwiftData | makes a class a persistable model |
@Generable | FoundationModels | makes a type structurally generatable by an LLM |
@Test / #expect | Swift Testing | defines tests |
#Preview | SwiftUI | defines a preview |
#Predicate | Foundation | type-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.
class Parent { var child: Child? }
class Child { weak var parent: Parent? } // weak breaks the cycleSame story when a closure captures self:
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
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
// 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
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:
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:
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:
@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
structorenumwhose members are all value types or immutable →Sendableautomatically - A
classneeds manual guarantees (finalplus immutable properties, or@unchecked Sendablewith your own locking) - An
actoris inherentlySendable
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:
// 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.
// 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:
swift package init --type library
swift build
swift test
swift run6.3 Packages worth knowing
- Swift Package Index — now part of Apple, and the authoritative place to find Swift packages
swift-collections—Deque,OrderedSet, and other containers the stdlib lacksswift-algorithms—chunked,windows, and other sequence algorithmsswift-async-algorithms— composition operators forAsyncSequencefoundation-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.
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:
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
// 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 sheet7.4 Modifiers
A modifier returns a new view wrapping the original. So order matters:
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 backgroundCommon modifiers by category:
// 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 27alertandconfirmationDialognow support the sameitem:binding style as sheets — set the bound value and the presentation appears, no separateBoolto maintain.
7.5 Containers and lists
// 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 aListprivilege; nowLazyVGrid,LazyHStack, and arbitrary containers use the same API, and watchOS supports reordering for the first time.swipeActionsContainerenables swipe actions across an entireScrollView.
7.6 Previews
#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
| Wrapper | Used for | Who owns the data |
|---|---|---|
@State | simple state private to a view | the current view |
@Binding | a read-write reference passed down from a parent | the parent view |
@Environment | shared data read from the environment | an ancestor view or the system |
@Bindable | getting bindings from an @Observable object | somewhere else |
@AppStorage | a setting synced with UserDefaults | system preferences |
8.2 @State and @Binding
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 27Statebecame a macro. Class instances stored in@Stateare now lazily initialized and created exactly once per view lifetime. The old problem of@Stateinitial 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.
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:
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:
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:
// 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:
ContentView().environment(store) // inject
@Environment(TodoStore.self) private var store // readThe system also provides plenty of environment values:
@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 locale8.5 @AppStorage
The simplest option for lightweight settings:
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
// 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
.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 withToolbarItem(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.
9.3 Search
@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.
// 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
// 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
.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:
// 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.
Image(systemName: "heart.fill")
.font(.title)
.symbolRenderingMode(.hierarchical) // hierarchical rendering
.foregroundStyle(.red, .pink) // multicolor
.symbolVariant(.circle) // variants
.symbolEffect(.pulse) // effects10.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.
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.
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:
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:
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).
- Add a
Localizable.xcstringsto the project - Write
Text("Hello")as normal; Xcode extracts strings automatically - Add target languages and translate in the catalog editor
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
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 anyCodablecustom or third-party type be stored directly as a property, instead of being manually decomposed into primitive fields.
11.2 Configuring the container
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: Todo.self) // one line
}
}For more control:
let container = try ModelContainer(
for: Todo.self, Subtask.self,
configurations: ModelConfiguration(
isStoredInMemoryOnly: false,
cloudKitDatabase: .automatic // turn on iCloud sync
)
)11.3 Queries
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 asectionBy:argument to@Query(a KeyPath from the model root to a string property) and get pre-grouped results, ready forSection— no moreDictionary(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:
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
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:
@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 27ResultsObserverandHistoryObserverlet 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:
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
| Need | Approach |
|---|---|
| Settings, switches, last selection | @AppStorage / UserDefaults |
| Structured business data you need to query | SwiftData |
| Cross-device sync (same Apple Account) | SwiftData + CloudKit, or NSUbiquitousKeyValueStore |
| Data shared between users, server-side logic | CloudKit (public database) or your own backend |
| Passwords, tokens, secrets | Keychain |
| Files (images, documents, exports) | FileManager + Documents directory |
| Throwaway cache | URLCache / Caches directory / memory |
12.2 UserDefaults
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
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:
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.
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:
@main
struct MyDocApp: App {
var body: some Scene {
DocumentGroup(newDocument: TextDocument()) { file in
EditorView(document: file.$document)
}
}
}Substantially expanded in iOS 27the new Document API addsWritableDocument/ReadableDocumentprotocols supporting asynchronous, incremental disk I/O with progress reporting through Foundation'sSubprogress— large files no longer need to be read into memory at once.DocumentCreationSourceplusNewDocumentButtonlet 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
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:
// 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
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:
- No backend: everything local plus iCloud sync. Most utility apps fall here — consider it first.
- CloudKit: when you need cross-user sharing or public content. Zero ops, included with your developer account.
- BaaS (Supabase, Firebase, etc.): when you need a relational database, realtime subscriptions, or third-party auth.
- 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
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
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):
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:
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.
@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:
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)
CoreAILanguageModelandMLXLanguageModel(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
NaturalLanguageframework, 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
@Generablerather 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
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
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
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.
15.5 Related integration points
| Capability | Framework / API |
|---|---|
| Spotlight indexing | CoreSpotlight; LLM semantic search as of WWDC26 |
| Universal links / deep links | Associated Domains + .onOpenURL |
| Sharing into your app | Share Extension |
| Sharing out of your app | ShareLink / Transferable |
| Drag and drop | .draggable / .dropDestination |
| Background work | BackgroundTasks |
| Shortcuts automation | App Intents (free) |
| Control Center controls | ControlWidget (see 18.4) |
| Action button / Camera Control | App 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.
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
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
| Framework | Capability |
|---|---|
Speech | speech to text, with on-device recognition |
AVSpeechSynthesizer | text to speech |
Translation | system-level translation, with UI or as a pure API |
NaturalLanguage | tokenization, POS tagging, named entity recognition, language identification, sentiment analysis, word embeddings |
SoundAnalysis | sound event classification |
Music Understanding | new at WWDC26, on-device audio analysis across six dimensions |
VisionKit | ready-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
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
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):
- Workout zone APIs — heart rate zones and cycling power zones. See Accessing workout zone data
- Perimenopause and menopause recording and querying APIs. See Recording and querying menopausal state
Related: HealthKit, WorkoutKit (build workout plans and push them to Apple Watch), CoreMotion (accelerometer, gyroscope, step counting).
17.3 Weather
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
| Need | Framework |
|---|---|
| Let the user pick photos | PhotosPicker (SwiftUI-native, no photo library permission needed) |
| Read/write the photo library | PhotoKit |
| Custom camera | AVFoundation (AVCaptureSession) |
| System camera capture UI | UIImagePickerController bridge (or build your own with AVCaptureSession) |
| Generative images | ImagePlayground (Apple Intelligence — not a camera or picker) |
| Video playback | VideoPlayer (SwiftUI) / AVKit |
| Audio playback and recording | AVFoundation / AVAudioEngine |
| Image processing and filters | Core Image |
| Control Center playback integration | `NowPlaying` (new at WWDC26) |
// 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 libraryShazamKit— music recognition, and matching against your own audio fingerprint catalogMusic 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
// 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 / requirement | Notes |
|---|---|
| Privacy Manifest | You must declare the data types your app and its SDKs collect, and the reasons for certain API use |
AppTrackingTransparency | Cross-app tracking requires an explicit permission prompt |
DeclaredAgeRange | Get an age range from the system (without the exact birthday) to serve age-appropriate content |
PermissionKit | A unified flow for children requesting parental approval |
FamilyControls / ManagedSettings / DeviceActivity | The official frameworks for Screen Time-style apps |
| Time Allowances | New 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
| Need | Framework |
|---|---|
| Bluetooth peripherals | CoreBluetooth |
| NFC tags | CoreNFC |
| Network state and custom protocols | Network |
| Local network discovery | Network (Bonjour) |
| Accessory communication | ExternalAccessory / AccessorySetupKit |
| Smart home | HomeKit / Matter |
| Haptics | CoreHaptics / .sensoryFeedback() |
| Screen mirroring and casting | AVRoutePickerView / AirPlay |
17.10 System services
| Need | Framework |
|---|---|
| Local and remote notifications | UserNotifications |
| Background work | BackgroundTasks |
| Large asset delivery | `BackgroundAssets` (self-hosted, or Apple-Hosted with 200 GB of hosting per app included with the developer account) |
| Calendar and reminders | EventKit |
| Contacts | Contacts |
| File selection | .fileImporter / .fileExporter (SwiftUI-native) |
| Watch/play together | GroupActivities (SharePlay) |
| In-app rating prompt | StoreKit's requestReview |
| Clipboard | UIPasteboard / PasteButton (SwiftUI) |
Important changeOn-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
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 27widgets can be customized through App Intents and support dynamic styling.
18.2 Interactive widgets
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.
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.
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:
// 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:
// 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 27the session "Compose advanced graphics effects with SwiftUI" covers stronger effect-composition capabilities alongside the refreshed material system.
19.3 2D and 3D
| Need | Recommendation |
|---|---|
| 2D games / particle effects | SpriteKit |
| 3D content (new projects) | `RealityKit` |
| 3D content (legacy projects) | SceneKit (deprecated; don't use for new work) |
| Low-level graphics / custom render pipelines | Metal |
| Game Center, achievements, leaderboards | GameKit |
| Controller support | GameController |
| Spatial audio | PHASE |
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
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
@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.
| Platform | Core interaction | Unique capabilities | Design notes |
|---|---|---|---|
| iOS | touch, one-handed | camera, sensors, Live Activities, Dynamic Island | single-column layouts, large tap targets, actions near the bottom |
| iPadOS | touch + keyboard/mouse + Apple Pencil | multiple windows, Slide Over, external displays, PencilKit | multi-column layouts, keyboard shortcuts, drag and drop |
| macOS | keyboard and mouse | menu bar, multiple windows, Settings scene, command line, background residency | high information density, context menus and shortcuts |
| watchOS | Digital Crown + touch, very short sessions | complications, workout sessions, notification forwarding, WorkoutKit | one thing per screen, large type, complications as the entry point |
| tvOS | remote-based focus navigation | TVUIKit, Top Shelf | focus-driven, 10-foot viewing distance, avoid text entry |
| visionOS | eye tracking + hand gestures | immersive spaces, spatial audio, object tracking | respect the user's physical environment, avoid blocking their view |
Organizing cross-platform code:
#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:
@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.
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:
// 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:
- File → New → File → StoreKit Configuration File
- Configure products, prices, and subscription groups in it
- Scheme → Edit Scheme → Run → Options → select it under StoreKit Configuration
- 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:
| Capability | What it is | Timing |
|---|---|---|
| Retention Messaging | Show 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 interaction | This fall |
| Subscription Bundles / Suites | Bundle: buy several existing subscriptions in one purchase. Suite: a set of subscriptions not sold individually, packaged as one | Details this summer |
| Group Purchases | A subscriber buys multiple seats and invites others; Apple provides the invitation flow | Later this year |
| Volume Purchasing | Sell to enterprise and education buyers through Apple School Manager and Apple Business Manager | This fall |
| Monthly with a 12-month commitment | A cheaper monthly rate in exchange for a one-year commitment; users can see completed and remaining payments in their Apple Account | Available now (requires iOS 26.4+, excluding the US and Singapore) |
| Unified in-app purchase submission | Group multiple IAPs, subscriptions, events, and custom product pages into one submission | This 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.
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
bodyto 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 asContentBuilder) 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 approvedPractical 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
| Need | Framework |
|---|---|
| Cross-platform UI | SwiftUI |
| Charts | Swift Charts |
| Feature discovery tips | TipKit |
| Widgets / complications | WidgetKit |
| Live Activities / Dynamic Island | ActivityKit |
| Control Center controls | WidgetKit (ControlWidget) |
| Handwriting and drawing | PencilKit |
| Rich text editing | SwiftUI TextEditor + AttributedString |
| Web content | WebKit |
| Data observation | Observation |
Data
| Need | Framework |
|---|---|
| Local database | SwiftData |
| Settings | @AppStorage / UserDefaults |
| iCloud sync | SwiftData + CloudKit |
| Backend services | CloudKit |
| Secrets | Keychain (Security) |
| Files and documents | FileManager / DocumentGroup |
| Networking | URLSession / Network |
Intelligence
| Need | Framework |
|---|---|
| On-device LLM | FoundationModels |
| Running your own models | Core AI |
| Evaluating AI features | Evaluations |
| Traditional ML models | Core ML / Create ML |
| Model training and fine-tuning | MLX |
| Image understanding | Vision / VisionKit |
| Speech recognition | Speech |
| Translation | Translation |
| Text analysis | NaturalLanguage |
| Audio analysis | SoundAnalysis / Music Understanding |
| System integration and Siri | App Intents |
| Search indexing | CoreSpotlight |
System capabilities
| Need | Framework |
|---|---|
| Maps and location | MapKit / CoreLocation |
| Health and fitness | HealthKit / WorkoutKit / CoreMotion |
| Weather | WeatherKit |
| Camera and media | AVFoundation / PhotoKit / Core Image |
| Playback integration | NowPlaying |
| Music | MusicKit / ShazamKit |
| Notifications | UserNotifications |
| Background work | BackgroundTasks / BackgroundAssets |
| Calendar and contacts | EventKit / Contacts |
| Payments | StoreKit / PassKit |
| Sign-in and security | AuthenticationServices / LocalAuthentication / CryptoKit |
| Privacy and parental controls | AppTrackingTransparency / DeclaredAgeRange / PermissionKit / FamilyControls |
| Bluetooth / NFC / accessories | CoreBluetooth / CoreNFC / AccessorySetupKit |
| Smart home | HomeKit / Matter |
| Haptics | CoreHaptics |
| Watch/play together | GroupActivities |
Graphics and space
| Need | Framework |
|---|---|
| 3D content | RealityKit + Reality Composer Pro |
| 2D games | SpriteKit |
| Low-level graphics | Metal |
| Environment understanding / AR | ARKit |
| Spatial content preview | Spatial Preview |
| Foveated streaming | Foveated Streaming |
| Game services | GameKit / GameController |
| Spatial audio | PHASE |
Engineering
| Need | Tool |
|---|---|
| Unit tests | Swift Testing |
| UI tests | XCUITest |
| Dependency management | Swift Package Manager |
| Documentation generation | DocC |
| Performance profiling | Instruments |
| CI/CD | Xcode 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 beginner | Learn this instead |
|---|---|
| Objective-C | Swift |
| UIKit / AppKit | SwiftUI |
| Core Data | SwiftData |
ObservableObject / @Published / @StateObject | @Observable + @State |
NavigationView | NavigationStack / NavigationSplitView |
| SceneKit | RealityKit |
| StoreKit 1 (the receipt-validation world) | StoreKit 2 |
| XCTest (for unit tests) | Swift Testing (UI tests still use XCUITest) |
| CocoaPods / Carthage | Swift Package Manager |
DispatchQueue / callback hell | async/await + actors |
| On-Demand Resources (deprecated in iOS 27) | Apple-Hosted Background Assets |
| WatchKit's UI layer | SwiftUI for watchOS |
| Storyboards / XIBs | SwiftUI (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:
- Side effects inside `body`. Network requests and database writes belong in
.task {}. - Overusing `GeometryReader`. It fills whatever space the parent offers, which frequently wrecks layouts. Try
ViewThatFits,.containerRelativeFrame, orGridfirst. - Force unwrapping with `!`. Half the crash reports after launch trace back to this.
- Ignoring `Sendable` warnings and papering over them with
@unchecked. Those warnings are protecting you. - Building elaborate architecture up front. Speed is a solo developer's biggest advantage; don't cancel it out yourself.
- Reimplementing system components. The system share sheet, subscription page, and photo picker are already good, and they improve automatically with OS updates.
- 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.
- 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)
- Apple Developer Documentation — authoritative and complete; check here first when unsure
- Technology Overviews — overviews by topic, good for finding entry points
- SwiftUI Tutorials — free official interactive tutorials, the best starting point
- Human Interface Guidelines — required reading for Apple platforms
- WWDC videos — a new batch every June, filterable by topic. The WWDC26 Guides page (
developer.apple.com/wwdc26/guides/) is the best annual navigation - Swift.org — the language's official site, including the full Swift Programming Language book
- Sample Code — Apple's official sample projects; the code quality is high
- Apple Developer Forums — Apple engineers answer questions here
Community
- Hacking with Swift (Paul Hudson) — excellent free tutorials;
100 Days of SwiftUIis the best-known entry path - Swift with Majid — in-depth SwiftUI writing; the post-WWDC breakdowns each year are solid
- Fatbobman's Blog / Swift Weekly — high-quality deep dives on SwiftData and SwiftUI
- Swift Package Index — finding packages (now part of Apple)
- Point-Free — deep functional and architectural content; save it for later
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`.