Claude Code for iOS Development: Swift, SwiftUI, and Apple Frameworks — Claude Skills 360 Blog
Blog / Mobile / Claude Code for iOS Development: Swift, SwiftUI, and Apple Frameworks
Mobile

Claude Code for iOS Development: Swift, SwiftUI, and Apple Frameworks

Published: December 6, 2026
Read time: 9 min read
By: Claude Skills 360

SwiftUI is Apple’s declarative UI framework — it integrates deeply with Swift’s type system, async/await for network calls, and @Observable for state management. SwiftData (built on Core Data) provides persistence with CloudKit sync. WidgetKit adds home screen widgets. Claude Code generates SwiftUI views, Swift Concurrency networking layers, SwiftData models, and the complete project structure for App Store submission.

CLAUDE.md for iOS Projects

## iOS Stack
- Xcode 16 / iOS 17+ deployment target
- UI: SwiftUI (no UIKit — if UIKit is needed, wrap in UIViewRepresentable)
- State: @Observable macro (Swift 5.9+) — not ObservableObject
- Networking: async/await with URLSession, no Combine
- Persistence: SwiftData (new) or Core Data (migration path)
- Dependencies: Swift Package Manager only — no CocoaPods/Carthage
- Testing: XCTest for unit, UI testing with XCUIApplication
- CI: Xcode Cloud or Fastlane + GitHub Actions
- Minimum target: iOS 17 (allows SwiftData and @Observable)

App Structure with @Observable

// Models/OrderStore.swift
import Foundation
import SwiftData
import Observation

// @Observable macro (Swift 5.9) — simpler than ObservableObject
@Observable
final class OrderStore {
    var orders: [Order] = []
    var isLoading = false
    var error: String? = nil
    
    private let apiClient: APIClient
    
    init(apiClient: APIClient = .shared) {
        self.apiClient = apiClient
    }
    
    @MainActor
    func loadOrders() async {
        isLoading = true
        defer { isLoading = false }
        
        do {
            orders = try await apiClient.fetchOrders()
            error = nil
        } catch {
            self.error = error.localizedDescription
        }
    }
    
    @MainActor
    func createOrder(_ input: CreateOrderInput) async throws -> Order {
        let order = try await apiClient.createOrder(input)
        orders.insert(order, at: 0)
        return order
    }
}

SwiftUI Navigation

// App/ContentView.swift — NavigationStack with typed destinations
import SwiftUI

enum AppDestination: Hashable {
    case orderDetail(Order)
    case createOrder
    case profile
}

struct ContentView: View {
    @State private var path = NavigationPath()
    @State private var store = OrderStore()
    
    var body: some View {
        NavigationStack(path: $path) {
            OrdersListView(store: store) { order in
                path.append(AppDestination.orderDetail(order))
            }
            .navigationTitle("Orders")
            .toolbar {
                ToolbarItem(placement: .primaryAction) {
                    Button("New Order") {
                        path.append(AppDestination.createOrder)
                    }
                }
                ToolbarItem(placement: .navigationBarLeading) {
                    Button(action: { path.append(AppDestination.profile) }) {
                        Image(systemName: "person.circle")
                    }
                }
            }
            .navigationDestination(for: AppDestination.self) { destination in
                switch destination {
                case .orderDetail(let order):
                    OrderDetailView(order: order)
                case .createOrder:
                    CreateOrderView(store: store)
                case .profile:
                    ProfileView()
                }
            }
        }
        .task {
            await store.loadOrders()
        }
    }
}

SwiftUI Views

// Views/OrdersListView.swift
import SwiftUI

struct OrdersListView: View {
    let store: OrderStore
    let onSelect: (Order) -> Void
    
    @State private var searchText = ""
    
    var filteredOrders: [Order] {
        if searchText.isEmpty { return store.orders }
        return store.orders.filter {
            $0.id.localizedCaseInsensitiveContains(searchText) ||
            $0.customerName.localizedCaseInsensitiveContains(searchText)
        }
    }
    
    var body: some View {
        Group {
            if store.isLoading && store.orders.isEmpty {
                ProgressView("Loading orders...")
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
            } else if let error = store.error {
                ContentUnavailableView(
                    "Unable to Load Orders",
                    systemImage: "exclamationmark.triangle",
                    description: Text(error)
                )
            } else if filteredOrders.isEmpty {
                ContentUnavailableView.search(text: searchText)
            } else {
                List(filteredOrders) { order in
                    OrderRowView(order: order)
                        .contentShape(Rectangle())
                        .onTapGesture { onSelect(order) }
                        .swipeActions(edge: .trailing) {
                            Button("Cancel", role: .destructive) {
                                Task { await store.cancelOrder(order.id) }
                            }
                        }
                }
                .refreshable {
                    await store.loadOrders()
                }
            }
        }
        .searchable(text: $searchText, prompt: "Search orders")
    }
}

struct OrderRowView: View {
    let order: Order
    
    var body: some View {
        HStack {
            VStack(alignment: .leading, spacing: 4) {
                Text("#\(order.id.suffix(6))")
                    .font(.headline)
                Text(order.customerName)
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
            }
            
            Spacer()
            
            VStack(alignment: .trailing, spacing: 4) {
                Text(order.totalAmount, format: .currency(code: "USD"))
                    .font(.headline)
                StatusBadge(status: order.status)
            }
        }
        .padding(.vertical, 4)
    }
}

struct StatusBadge: View {
    let status: OrderStatus
    
    var color: Color {
        switch status {
        case .pending: .orange
        case .processing: .blue
        case .shipped: .purple
        case .delivered: .green
        case .cancelled: .red
        }
    }
    
    var body: some View {
        Text(status.rawValue.capitalized)
            .font(.caption)
            .fontWeight(.medium)
            .padding(.horizontal, 8)
            .padding(.vertical, 3)
            .background(color.opacity(0.15))
            .foregroundStyle(color)
            .clipShape(.capsule)
    }
}

SwiftData Model

// Models/Persistence/OrderModel.swift
import SwiftData
import Foundation

@Model
final class OrderModel {
    @Attribute(.unique) var id: String
    var customerName: String
    var status: String  // Raw value of OrderStatus enum
    var totalCents: Int
    var createdAt: Date
    var updatedAt: Date
    
    @Relationship(deleteRule: .cascade)
    var items: [OrderItemModel] = []
    
    init(id: String, customerName: String, status: String, totalCents: Int, createdAt: Date, updatedAt: Date) {
        self.id = id
        self.customerName = customerName
        self.status = status
        self.totalCents = totalCents
        self.createdAt = createdAt
        self.updatedAt = updatedAt
    }
}

@Model
final class OrderItemModel {
    var productId: String
    var productName: String
    var quantity: Int
    var unitPriceCents: Int
    var order: OrderModel?
    
    init(productId: String, productName: String, quantity: Int, unitPriceCents: Int) {
        self.productId = productId
        self.productName = productName
        self.quantity = quantity
        self.unitPriceCents = unitPriceCents
    }
}

// App entry point with SwiftData container
@main
struct MyApp: App {
    let container: ModelContainer
    
    init() {
        do {
            container = try ModelContainer(for: OrderModel.self, OrderItemModel.self, configurations: ModelConfiguration(
                isStoredInMemoryOnly: false,
                cloudKitDatabase: .automatic  // Enable CloudKit sync
            ))
        } catch {
            fatalError("Failed to create ModelContainer: \(error)")
        }
    }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(container)
    }
}

Swift Concurrency Networking

// Network/APIClient.swift
import Foundation

actor APIClient {
    static let shared = APIClient()
    
    private let session: URLSession
    private let decoder: JSONDecoder
    private let baseURL = URL(string: "https://api.myapp.com/v1")!
    
    private init() {
        let config = URLSessionConfiguration.default
        config.timeoutIntervalForRequest = 30
        config.waitsForConnectivity = true
        self.session = URLSession(configuration: config)
        
        self.decoder = JSONDecoder()
        self.decoder.dateDecodingStrategy = .iso8601
        self.decoder.keyDecodingStrategy = .convertFromSnakeCase
    }
    
    func fetchOrders() async throws -> [Order] {
        try await get("/orders")
    }
    
    func createOrder(_ input: CreateOrderInput) async throws -> Order {
        try await post("/orders", body: input)
    }
    
    private func get<T: Decodable>(_ path: String) async throws -> T {
        let request = try buildRequest(method: "GET", path: path)
        return try await perform(request)
    }
    
    private func post<Body: Encodable, T: Decodable>(_ path: String, body: Body) async throws -> T {
        var request = try buildRequest(method: "POST", path: path)
        request.httpBody = try JSONEncoder().encode(body)
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        return try await perform(request)
    }
    
    private func perform<T: Decodable>(_ request: URLRequest) async throws -> T {
        let (data, response) = try await session.data(for: request)
        
        guard let http = response as? HTTPURLResponse else {
            throw APIError.invalidResponse
        }
        
        guard 200...299 ~= http.statusCode else {
            let apiError = try? decoder.decode(APIErrorResponse.self, from: data)
            throw APIError.httpError(statusCode: http.statusCode, message: apiError?.message)
        }
        
        return try decoder.decode(T.self, from: data)
    }
    
    private func buildRequest(method: String, path: String) throws -> URLRequest {
        var request = URLRequest(url: baseURL.appending(path: path))
        request.httpMethod = method
        
        if let token = AuthStore.shared.accessToken {
            request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        }
        
        return request
    }
}

For the React Native alternative to native iOS development with code sharing to Android, see the React Native Expo guide. For the REST API that this Swift networking layer communicates with, the FastAPI guide covers Python API patterns. The Claude Skills 360 bundle includes iOS/Swift skill sets covering SwiftUI views, Swift Concurrency, and SwiftData persistence. Start with the free tier to try SwiftUI view generation.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free