Native iOS and macOS apps in Swift 6 - SwiftUI, SwiftData, concurrency, Swift Testing, XcodeGen projects, CallKit and audio sessions, App Intents and the Action Button, devices, TestFlight, notarization. Use for any iPhone or Mac app work.
Installs into .claude/skills of the current project.
Are you the author of Swift Dev?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/tenequm-swift-dev)
---
name: swift-dev
description: Native iOS and macOS apps in Swift 6 - SwiftUI, SwiftData, concurrency, Swift Testing, XcodeGen projects, CallKit and audio sessions, App Intents and the Action Button, devices, TestFlight, notarization. Use for any iPhone or Mac app work.
metadata:
version: "0.1.0"
categories: "development"
topics: "swift, swiftui, ios, macos, xcode"
upstream: "swift@6.4, xcode@27.0, ios@27.0, macos@27.0"
openclaw:
homepage: https://github.com/tenequm/skills/tree/main/skills/swift-dev
emoji: "🍎"
os:
- macos
---
# 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](#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 generate` are silently left out of the build (even with syntax errors) until you regenerate - use the `syncedFolder` source type or regenerate before every build.
- The generated Info.plist hardcodes version 1.0 (1) and ignores `MARKETING_VERSION` / `CURRENT_PROJECT_VERSION` unless you map them in `info.properties`.
- Gitignoring all of `*.xcodeproj/` also drops `Package.resolved`, so clean clones float to the newest dependency versions. Keep that one file committed.
- XcodeGen's defaults are `SWIFT_VERSION 5.0` and iPhone+iPad; set both explicitly.
### Build and test from the command line
```bash
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 does not work in the Simulator - gate it with `#if targetEnvironment(simulator)`.
## Concurrency (Swift 6 language mode)
Swift 6 mode makes data races compile errors. Two module-level defaults decide most of the friction:
```swift
// 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). `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.
```swift
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`/`Hashable` conformances become main-actor isolated, so decoding it in `@concurrent` code (or storing it with `@Attribute(.codable)`) fails with `main actor-isolated conformance of 'Report' to 'Decodable' cannot be used in @concurrent context`. Declare plain data types `nonisolated struct`.
- A plain `nonisolated async` function runs on the caller's actor (Swift 6.2+ behavior). Use `@concurrent` when you need it off the main actor.
- Value types of Sendable members are implicitly `Sendable`; a `final class` with only `let` Sendable properties can declare it. Otherwise use an `actor` or `Mutex` (Synchronization).
- Typed throws: `func load() async throws(LoadError) -> T` gives callers an exhaustive `catch`; 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:
```swift
@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 no `nonisolated` (rejected for protocols that require `Sendable`). Use that form when an action must be fulfilled after an `await` (e.g. `CXSetMutedCallAction` once the mic change succeeds): carrying a `CXAction` into a `Task` from a `nonisolated` method fails with `sending 'action' risks causing data races`. Inside `assumeIsolated`, write `_ = Task { ... }` - a bare `Task` is the closure's unused result and warns.
- Delegate on another thread (LiveKit `RoomDelegate`, which is `Sendable` and 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, an `await` inside 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 one `refresh()`), and copy Sendable values (IDs, Bools) out of non-Sendable arguments such as `CXAction` before 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
```swift
@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
```swift
@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 ("key path cannot refer to enum case") - capture `let high = Priority.high` first. Optional chains through composite attributes (`$0.address?.city`) crash at runtime.
- The SwiftUI `.modelContainer(...)` modifiers take no `migrationPlan:`; build `ModelContainer(for:migrationPlan:configurations:)` yourself and pass it in. `VersionedSchema.versionIdentifier` must be `static let` in Swift 6.
- `ModelConfiguration` defaults to `cloudKitDatabase: .automatic`, which turns on sync as soon as the target has any CloudKit entitlement. Pass `.none` unless you want sync; with sync, every property must be optional or defaulted, no `.unique`, relationships optional.
- `ModelContext` is not Sendable. A `@ModelActor` awaited from main-actor code ran on the main thread in testing; call it from `@concurrent` code for real background work.
Models, predicates, sectioned queries: `swiftdata-models.md`. Containers, background contexts, observers, migrations, CloudKit: `swiftdata-store.md`.
## Swift Testing
```swift
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 a `public` struct from another module fails with a misleading overload error - `cannot convert ... to '(any (~Copyable & ~Escapable).Type)?'` when `x` is optional, an `AttributedStringProtocol` or `'Grant' and '()'` error otherwise (inside or outside `#expect`). The cause: synthesized memberwise initializers are `internal`. Spelling out the type (`Grant(a:b:)`) shows the real error. Fix with `@testable import` (on by default under `swift test`), an explicit `public init`, or by comparing fields.
- A package target built with `.defaultIsolation(MainActor.self)` makes its API `@MainActor`, so its tests must be `@MainActor` too. Keep logic packages nonisolated by default.
- An iOS app target's tests need a simulator runtime (36 s warm through `xcodebuild` versus 1.4 s for the same tests under `swift test`). Exit tests are unavailable on iOS.
Exit tests, attachments, XCTest interop, UI and simulator tests: `testing.md`.
## Swift packages
```swift
// 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"]),
]
)
```
Plugins, macros, Swift Build, `xcodebuild` basics 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+)
```swift
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: `UIDesignRequiresCompatibility` is ignored.
- **ld64 is removed**: `-ld_classic` is now ignored with a warning (fatal only under warnings-as-errors); delete it from `OTHER_LDFLAGS`.
- **SwiftPM 6.4 builds with Swift Build**: products land in `.build/out/Products/<Config>` (`.build/release` is a symlink); hardcoded `.build/arm64-apple-macosx/release` paths break.
- **macOS universal binaries**: `ARCHS_STANDARD` drops x86_64 when the deployment target is macOS 27+.
- **Export methods renamed**: `app-store` / `ad-hoc` / `development` are deprecated; use `app-store-connect` / `release-testing` / `debugging`.
- **Device registration** from the command line needs `-allowProvisioningDeviceRegistration` in addition to `-allowProvisioningUpdates`.
- **Audio interruptions**: `AVAudioSession.interruptionNotification` is deprecated on iOS 27 in favor of typed `didBecomeActive` / `didBecomeInactive` messages.
- **Documents**: new `Document` (`ReadableDocument` + `WritableDocument`) protocols with `@concurrent` read/write; `FileDocument` is soft-deprecated.
## 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, 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 | `testing.md` |
| Package.swift, plugins, macros, Swift Build, build-breakers | `spm-and-builds.md` |
| Generic-password helper, accessibility classes | `keychain.md` |
| Foundation Models (on-device and Private Cloud Compute), Core AI | `foundation-models.md` |