Skip to main content
< All Topics
Print

Chapter 17: iOS & macOS with Swift

Chapter 17: iOS & macOS with Swift

Last Updated: 2026-03

## 17.1 Overview

Swift and SwiftUI are used for native Apple platform applications. ITI’s primary Swift project is expat-advisor (Personal/expat-advisor/), a native macOS app for expatriate planning.

Swift apps use:

SwiftUI — declarative UI framework for macOS and iOS

CloudKit — iCloud-based sync and cloud storage

Core Data — local data persistence

ITI Shared Swift LibraryClaudeService.swift and KeychainService.swift

17.2 ClaudeService

ClaudeService.swift from ITI/shared/ios-macos/ provides a complete Anthropic Claude client for Swift.

Initialization


// Reads ANTHROPIC_API_KEY from Keychain
let claude = ClaudeService.shared

Non-streaming completion


let response = try await ClaudeService.shared.complete(
    system: "You are an expert expat advisor.",
    user: "What are the tax implications of moving to Portugal?"
)
print(response.content)

Streaming completion (for real-time UI updates)


for try await chunk in ClaudeService.shared.stream(
    system: "You are an expert expat advisor.",
    user: "What are the top 5 things to consider when moving abroad?"
) {
    await MainActor.run {
        self.responseText += chunk
    }
}

Tool use


let tools: [ClaudeTool] = [
    ClaudeTool(
        name: "search_tax_treaties",
        description: "Search for tax treaty information between two countries",
        parameters: [
            "country_a": .string(description: "Origin country"),
            "country_b": .string(description: "Destination country"),
        ]
    )
]

let response = try await ClaudeService.shared.complete(
    system: "You are an expert expat advisor.",
    user: "What are the tax implications of an American moving to Portugal?",
    tools: tools
)

17.3 KeychainService

KeychainService.swift from ITI/shared/ios-macos/ provides secure credential storage using the system Keychain.


// Store an API key
try KeychainService.store(key: "ANTHROPIC_API_KEY", value: apiKey)

// Retrieve an API key
let apiKey = try KeychainService.retrieve(key: "ANTHROPIC_API_KEY")

// Delete a key
try KeychainService.delete(key: "ANTHROPIC_API_KEY")

Warning: Never store API keys in UserDefaults, .plist files, or source code. Always use KeychainService.


17.4 SwiftUI Patterns

MVVM architecture

ITI SwiftUI apps follow the Model-View-ViewModel pattern:


// ViewModel
@MainActor
class ExpatAdvisorViewModel: ObservableObject {
    @Published var response: String = ""
    @Published var isLoading: Bool = false
    @Published var error: String?

    func analyze(query: String) async {
        isLoading = true
        error = nil
        do {
            response = try await ClaudeService.shared.complete(
                system: ExpatAdvisorPrompts.system,
                user: query
            ).content
        } catch {
            self.error = error.localizedDescription
        }
        isLoading = false
    }
}

// View
struct ExpatAdvisorView: View {
    @StateObject private var viewModel = ExpatAdvisorViewModel()
    @State private var query: String = ""

    var body: some View {
        VStack {
            TextField("Ask about expat planning...", text: $query)
            Button("Analyze") {
                Task { await viewModel.analyze(query: query) }
            }
            .disabled(viewModel.isLoading)

            if viewModel.isLoading {
                ProgressView()
            } else {
                Text(viewModel.response)
            }
        }
        .padding()
    }
}

Async/await with Task


Button("Get Advice") {
    Task {
        await viewModel.analyze(query: inputText)
    }
}

17.5 Core Data

Core Data is used for local persistent storage in macOS apps.

Setup (in App entry point)


@main
struct ExpatAdvisorApp: App {
    let persistenceController = PersistenceController.shared

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.managedObjectContext, persistenceController.container.viewContext)
        }
    }
}

PersistenceController


class PersistenceController {
    static let shared = PersistenceController()

    let container: NSPersistentContainer

    init() {
        container = NSPersistentContainer(name: "ExpatAdvisor")
        container.loadPersistentStores { _, error in
            if let error = error {
                fatalError("Core Data store failed: \(error)")
            }
        }
        container.viewContext.automaticallyMergesChangesFromParent = true
    }
}

17.6 CloudKit Integration

CloudKit provides iCloud sync for macOS apps that need cross-device data sharing.

Enable CloudKit

In Xcode: Target > Signing & Capabilities > + Capability > iCloud > enable CloudKit.

Using NSPersistentCloudKitContainer (Core Data + CloudKit)


// Replace NSPersistentContainer with NSPersistentCloudKitContainer
container = NSPersistentCloudKitContainer(name: "ExpatAdvisor")

This automatically syncs Core Data to iCloud with zero additional code.

CloudKit record operations (direct)


import CloudKit

let database = CKContainer.default().privateCloudDatabase

// Save a record
let record = CKRecord(recordType: "UserProfile")
record["name"] = "John Doe" as CKRecordValue
try await database.save(record)

// Fetch records
let query = CKQuery(recordType: "UserProfile", predicate: NSPredicate(value: true))
let (results, _) = try await database.records(matching: query)

17.7 Building and Distributing

Build from Xcode

  1. Open the .xcodeproj or .xcworkspace file in Xcode.
  2. Select the target scheme.
  3. Choose Product > Build (⌘B) or Product > Archive for distribution.

Build from command line


# Build for macOS
xcodebuild -project ExpatAdvisor.xcodeproj \
  -scheme ExpatAdvisor \
  -configuration Release \
  -archivePath build/ExpatAdvisor.xcarchive \
  archive

Previous: Chapter 16 — Python Services | Next: Chapter 18 — Claude & the Anthropic API

Table of Contents