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.