Swift Apple Development (iOS + macOS)
Native apps for iPhone and Mac with Swift 6.4 and Xcode 27.0 (iOS 27.0 and macOS 27.0 SDKs). Xcode 27 runs only on Apple silicon and needs macOS 26.6 or later. Deployment targets: iOS 17 / macOS 14 for @Observable and SwiftData; iOS 26 / macOS 26 for Liquid Glass, Foundation Models, IntentModes and Observations.
This file is the platform-neutral core plus a router. Load a reference only when the task needs it - the Platform tables say which.
Start here
| Task | Default |
|---|---|
| New iOS app | XcodeGen project.yml (generated .xcodeproj gitignored) + logic in a local SPM package - ios-project-setup.md |
| New Mac app | XcodeGen for a full app; a bare SPM executable only for CLIs and small agents (manual .app assembly in spm-and-builds.md) |
| Logic, models, networking | A local SPM package with platforms: [.iOS(.v26), .macOS(.v15)], tested with swift test on the Mac - no simulator |
| Run on an iPhone | ios-devices-and-signing.md (Developer Mode, devicectl, automatic signing flags) |
| Ship | iOS: ios-distribution.md (TestFlight, App Store). Mac: macos-distribution.md (Developer ID + notarization, Mac App Store) |
SPM alone cannot produce an iOS .app - an app target needs an Xcode project, which is why XcodeGen generates one. XcodeGen traps (details and fixes in ios-project-setup.md):
- Files added after
xcodegen generateare silently left out of the build (even with syntax errors) until you regenerate - use thesyncedFoldersource type or regenerate before every build. - The generated Info.plist hardcodes version 1.0 (1) and ignores
MARKETING_VERSION/CURRENT_PROJECT_VERSIONunless you map them ininfo.properties. - Gitignoring all of
*.xcodeproj/also dropsPackage.resolved, so clean clones float to the newest dependency versions. Keep that one file committed. - XcodeGen's defaults are
SWIFT_VERSION 5.0and iPhone+iPad; set both explicitly.
Build and test from the command line
swift test --package-path Core # logic, on the Mac, seconds
xcodegen generate # project.yml -> App.xcodeproj
xcodebuild -project App.xcodeproj -scheme App \
-destination 'generic/platform=iOS' CODE_SIGNING_ALLOWED=NO build # compile check, no signing
xcodebuild -project App.xcodeproj -scheme App \
-destination 'platform=iOS Simulator,name=iPhone 17' test # needs a simulator runtime
xcrun simctl list runtimes # empty on a fresh Xcode
xcodebuild -downloadPlatform iOS # installs the iOS simulator runtime (GBs)
xcrun devicectl list devices # simulators and paired iPhones
Simulator.app is called DeviceHub.app in Xcode 27. CallKit runs in the iOS 27 Simulator only with two Simulator-only stand-ins - claim the facetime URL scheme and activate the audio session yourself (ios-audio-and-callkit.md). UI tests in the Simulator can also press the Action Button, lock the screen and run App Intents out of process (testing.md). Starting a CallKit call or Live Activity from an App Intent while the app sits in the background needs the iOS 27 .phone.startCall App Schema; a plain AppIntent is refused (app-intents.md).
Concurrency (Swift 6 language mode)
Swift 6 mode makes data races compile errors. Two module-level defaults decide most of the friction:
// Package.swift target settings
swiftSettings: [
.swiftLanguageMode(.v6),
.defaultIsolation(MainActor.self), // app targets: everything is @MainActor unless marked
]
In Xcode the same is SWIFT_VERSION = 6 and SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor (plus SWIFT_APPROACHABLE_CONCURRENCY = YES, which the Xcode 27 app template sets - that template still says SWIFT_VERSION = 5.0, so switch it to 6). SWIFT_STRICT_CONCURRENCY does nothing in Swift 6 mode - checking is already complete. Make app targets MainActor by default; leave library packages nonisolated. Misspelled enableUpcomingFeature names are silently ignored.
nonisolated struct Report: Decodable { // see the conformance trap below
let title: String
}
@concurrent
func decode(_ data: Data) async throws -> Report { // always runs off the caller's actor
try JSONDecoder().decode(Report.self, from: data)
}
actor Cache { // mutable shared state
private var items: [URL: Data] = [:]
func value(for url: URL) -> Data? { items[url] }
func store(_ data: Data, for url: URL) { items[url] = data }
}
- Conformance trap under MainActor default isolation: a type's
Codable/Hashableconformances become main-actor isolated, so decoding it in@concurrentcode (or storing it with@Attribute(.codable)) fails withmain actor-isolated conformance of 'Report' to 'Decodable' cannot be used in @concurrent context. Declare plain data typesnonisolated struct. - A plain
nonisolated asyncfunction runs on the caller's actor (Swift 6.2+ behavior). Use@concurrentwhen you need it off the main actor. - Value types of Sendable members are implicitly
Sendable; afinal classwith onlyletSendable properties can declare it. Otherwise use anactororMutex(Synchronization). - Typed throws:
func load() async throws(LoadError) -> Tgives callers an exhaustivecatch; use it for domain errors you map at a boundary, not for everything.
Bridging delegates into a @MainActor class
CallKit, AVFoundation and SDK delegates call back on their own queues. Mark the delegate methods nonisolated, then get back onto the main actor:
@MainActor @Observable
final class CallController: NSObject, CXProviderDelegate {
private let provider: CXProvider
override init() {
provider = CXProvider(configuration: CXProviderConfiguration())
super.init()
provider.setDelegate(self, queue: nil) // nil = main queue, so assumeIsolated below is safe
}
nonisolated func providerDidReset(_ provider: CXProvider) {
MainActor.assumeIsolated { endCall() }
}
nonisolated func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
action.fulfill() // CXAction is not Sendable - finish with it here
MainActor.assumeIsolated { endCall() }
}
private func endCall() { /* update observable state */ }
}
- Delegate delivered on the main queue (you chose it):
MainActor.assumeIsolated- synchronous, traps if the premise is wrong. Shorter equivalent: an isolated conformance,extension CallController: @MainActor CXProviderDelegate, with nononisolated(rejected for protocols that requireSendable). Use that form when an action must be fulfilled after anawait(e.g.CXSetMutedCallActiononce the mic change succeeds): carrying aCXActioninto aTaskfrom anonisolatedmethod fails withsending 'action' risks causing data races. InsideassumeIsolated, write_ = Task { ... }- a bareTaskis the closure's unused result and warns. - Delegate on another thread (LiveKit
RoomDelegate, which isSendableand calls from an internal queue):Task { @MainActor in ... }. Tasks created that way from one serial queue start in order, but they are not ordered against callbacks from other queues, anawaitinside one lets later ones interleave, and the callback's arguments are stale by the time it runs. Make the handler idempotent (re-read the source of truth in onerefresh()), and copy Sendable values (IDs, Bools) out of non-Sendable arguments such asCXActionbefore the hop. - Verify concurrency code with a real build or
swiftc -emit-sil -o /dev/null, not-typecheck: region-isolation errors (sending 'x' risks causing data races) come from the SIL pass and never appear in a typecheck.
Deep dives: concurrency-isolation.md (isolation, Sendable, delegates, typed throws), concurrency-tasks.md (tasks, cancellation, streams, Observations), concurrency-migration.md (GCD/Combine to async, Swift 6 migration, Swift 6.4 changes).
State: @Observable controllers
@MainActor @Observable
final class AppModel {
static let shared = AppModel() // one instance that views and App Intents both reach
private(set) var items: [Item] = []
@ObservationIgnored private var loadTask: Task<Void, Never>?
func reload() {
loadTask?.cancel()
loadTask = Task { items = (try? await ItemService.fetchAll()) ?? [] }
}
}
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView().environment(AppModel.shared)
}
}
}
struct ContentView: View {
@Environment(AppModel.self) private var model
var body: some View {
List(model.items) { Text($0.title) }
.task { model.reload() }
}
}
Owner: @State var model = AppModel() or a shared instance; inject with .environment(model); read with @Environment(AppModel.self); bind with @Bindable var model = model. Mark non-UI fields @ObservationIgnored. Architecture options and DI: architecture.md.
SwiftData
@Model
final class Project {
var name: String
var createdAt: Date = Date.now
@Relationship(deleteRule: .cascade, inverse: \ProjectTask.project) var tasks: [ProjectTask] = []
init(name: String) { self.name = name } // @Model never synthesizes an init
}
@Model
final class ProjectTask { // not `Task` - that shadows Swift.Task
var title: String
var project: Project?
init(title: String) { self.title = title }
}
// In a view
@Query(filter: #Predicate<Project> { !$0.name.isEmpty }, sort: \Project.createdAt, order: .reverse)
private var projects: [Project]
// Scene: .modelContainer(for: [Project.self, ProjectTask.self])
// Tests: ModelContainer(for: Project.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true))
Traps that compile but break, or do not compile at all (all reproduced on the 27 SDKs):
#Predicate { $0.priority == .high }fails ("member access without an explicit base is not supported in this predicate"), and$0.priority == Priority.highfails with "key path cannot refer to enum case" - capturelet high = Priority.highfirst. Optional chains through composite attributes ($0.address?.city) crash at runtime.- The SwiftUI
.modelContainer(...)modifiers take nomigrationPlan:; buildModelContainer(for:migrationPlan:configurations:)yourself and pass it in.VersionedSchema.versionIdentifiermust bestatic letin Swift 6. ModelConfigurationdefaults tocloudKitDatabase: .automatic, which turns on sync as soon as the target has any CloudKit entitlement. Pass.noneunless you want sync; with sync, every property must be optional or defaulted, no.unique, relationships optional.ModelContextis not Sendable. A@ModelActorawaited from main-actor code ran on the main thread in testing; call it from@concurrentcode for real background work.
Models, predicates, sectioned queries: swiftdata-models.md. Containers, background contexts, observers, migrations, CloudKit: swiftdata-store.md.
Swift Testing
import Testing
@testable import Core
struct ParserTests {
@Test func parsesLink() throws {
let line = try #require(Line(link: "https://example.com/voice?t=abc"))
#expect(line.token == "abc")
}
@Test(arguments: ["", "http://x/voice?t=a", "https://x/voice"])
func rejects(_ link: String) {
#expect(Line(link: link) == nil)
}
}
Traps:
x == .init(a: 1, b: 2)against apublicstruct from another module that has some other public init (e.g. Decodable'sinit(from:)) fails with a misleading overload error -cannot convert ... to '(any (~Copyable & ~Escapable).Type)?'whenxis optional, anAttributedStringProtocolor'Grant' and '()'error otherwise (inside or outside#expect). The cause: synthesized memberwise initializers areinternal. Spelling out the type (Grant(a:b:)) shows the real error. Fix with@testable import(on by default underswift test), an explicitpublic init, or by comparing fields.- A package target built with
.defaultIsolation(MainActor.self)makes its API@MainActor, so its tests must be@MainActortoo. Keep logic packages nonisolated by default. - An iOS app target's tests need a simulator runtime (36 s warm through
xcodebuildversus 1.4 s for the same tests underswift test). Exit tests are unavailable on iOS. xcodebuildUI-test runs can stall for ~10 minutes after a failure (it spawnssimctl diagnose) or never exit after a pass - wrap them intimeoutand enable test timeouts (testing.md).
Exit tests, attachments, XCTest interop, UI and simulator tests: testing.md.
Swift packages
// swift-tools-version: 6.2
import PackageDescription
let package = Package(
name: "Core",
platforms: [.iOS(.v26), .macOS(.v15)],
products: [.library(name: "Core", targets: ["Core"])],
targets: [
.target(name: "Core", swiftSettings: [.swiftLanguageMode(.v6)]),
.testTarget(name: "CoreTests", dependencies: ["Core"]),
]
)
.iOS(.v27) / .macOS(.v27) need swift-tools-version: 6.4; older tools versions fail with 'v27' is unavailable. Plugins, macros, Swift Build, xcodebuild basics, the Xcode MCP server and Xcode 27 build-breakers: spm-and-builds.md.
Keychain
Store tokens and credentials with SecItemAdd / SecItemCopyMatching / SecItemUpdate as generic passwords. Use kSecAttrAccessibleAfterFirstUnlock when a background task or a locked-phone call must read the item; the default ...WhenUnlocked fails while locked. ...ThisDeviceOnly variants never leave the device (no backup restore, no migration). Update an existing item with SecItemUpdate rather than delete-then-add. On macOS the data protection keychain (kSecUseDataProtectionKeychain) fails with -34018 errSecMissingEntitlement unless the binary carries a provisioning profile - ad hoc and plain Apple Development signatures are not enough. Helper and macOS differences: keychain.md.
Foundation Models (iOS 26+ / macOS 26+)
import FoundationModels
@Generable
struct Summary {
@Guide(description: "At most five words") var title: String
var points: [String]
}
func summarize(_ text: String) async throws -> Summary? {
guard case .available = SystemLanguageModel.default.availability else { return nil }
let session = LanguageModelSession(instructions: "Summarize the user's text.")
return try await session.respond(to: text, generating: Summary.self).content
}
The on-device model has a 4096-token context; PrivateCloudComputeLanguageModel() (27+, managed entitlement) has 32K. Errors: LanguageModelError (iOS/macOS 27+) replaces the deprecated LanguageModelSession.GenerationError; apps that still deploy to 26 catch both behind #available. Custom adapters are unavailable at a 27.0 deployment target. Tools, streaming, image input, PCC and Core AI: foundation-models.md.
Xcode 27 / iOS 27 / macOS 27 changes that bite
- Liquid Glass is mandatory for apps built with the 27 SDKs:
UIDesignRequiresCompatibilityis ignored. - ld64 is removed:
-ld_classicis now ignored with a warning (fatal only under warnings-as-errors); delete it fromOTHER_LDFLAGS. - SwiftPM 6.4 builds with Swift Build: products land in
.build/out/Products/<Config>(.build/releaseis a symlink); hardcoded.build/arm64-apple-macosx/releasepaths break. - macOS universal binaries:
ARCHS_STANDARDdrops x86_64 when the deployment target is macOS 27+. - Export methods (renamed back in Xcode 15.3, still common in old scripts):
app-store/ad-hoc/developmentare deprecated; useapp-store-connect/release-testing/debugging. - Device registration from the command line needs
-allowProvisioningDeviceRegistrationin addition to-allowProvisioningUpdates. - Audio interruptions:
AVAudioSession.interruptionNotificationis deprecated on iOS 27 in favor of the typeddidBecomeInactiveandresumptionRecommendationmessages. Swift does not warn on leftoverinterruptionNotificationobservers - grep for them. - Documents: new class-bound
Document(ReadableDocument+WritableDocument) protocols whoseDocumentReader/DocumentWriterdo the@concurrentread/write;FileDocumentis soft-deprecated. - UIKit life cycle: apps built with the iOS 27 SDK must adopt the scene-based life cycle or they fail to launch.
Platform
iOS
| Topic | Reference |
|---|---|
| XcodeGen project, Info.plist keys, local package, build commands | ios-project-setup.md |
| WindowGroup, scenePhase, NavigationStack, sheets, toolbars, Settings deep link, Liquid Glass | ios-swiftui.md |
Simulator runtimes, simctl/devicectl, Developer Mode, CLI signing, ASC API keys |
ios-devices-and-signing.md |
| Dev install, ad hoc, TestFlight, App Store, certificates | ios-distribution.md |
AVAudioSession, mic permission, interruptions, CallKit outgoing calls, LiveKit, screen-locked audio |
ios-audio-and-callkit.md |
| App Intents, App Shortcuts, Action Button, Siri, App Schemas (background call start), Live Activities, Control Center controls (also macOS) | app-intents.md |
macOS
| Topic | Reference |
|---|---|
Scenes, windows, Settings, MenuBarExtra, documents, termination, LSUIElement |
macos-app-lifecycle.md |
| Table, sidebar, inspector, menus and commands, forms, popovers, 27-SDK SwiftUI changes | macos-swiftui.md |
NSViewRepresentable, hosting, AppKit Liquid Glass, pasteboard, NSPanel HUDs |
macos-appkit-interop.md |
| Shortcuts, drag and drop, bookmarks, AXUIElement, login items, XPC, logging, privacy strings | macos-system-integration.md |
ScreenCaptureKit (SCStream, filters, audio-only capture, TCC) |
macos-screencapturekit.md |
| Recording pipeline: AVAudioEngine, AVAssetWriter, SpeechAnalyzer, silent-recording traps | macos-audio-pipeline.md |
| Per-process audio with Core Audio taps | macos-core-audio-tap.md |
| Developer ID, notarization, sandbox, Mac App Store, Sparkle | macos-distribution.md |
Shared
| Topic | Reference |
|---|---|
| Isolation, actors, Sendable, delegate bridging, typed throws | concurrency-isolation.md |
| Tasks, cancellation, AsyncStream, Observations, continuations | concurrency-tasks.md |
| GCD/Combine migration, Swift 6 migration, Swift 6.4 features | concurrency-migration.md |
@Model, relationships, predicates, @Query |
swiftdata-models.md |
| Container, contexts, observers, migrations, CloudKit | swiftdata-store.md |
| App structure, controllers, DI | architecture.md |
Swift Testing, traps, UI and simulator tests (Action Button, lock, App Intents), xcodebuild stalls |
testing.md |
| Package.swift, plugins, macros, Swift Build, build-breakers, Xcode MCP server | spm-and-builds.md |
| Generic-password helper, accessibility classes | keychain.md |
| Foundation Models (on-device and Private Cloud Compute), Core AI | foundation-models.md |
