Guide

Swift fundamentals explained

A Harbor Fleet technician scans a dock label, captures a delivery photo, and queues the update while cellular signal drops. When LTE returns, the iOS client replays batched events to the sync API — written in Swift, Apple’s general-purpose language for iOS, iPadOS, macOS, watchOS, visionOS, and increasingly Linux server workloads. Swift replaced Objective-C as the default for new Apple platform code; Swift 6 (2024–2025) tightened data-race safety with compile-time concurrency checking. Where Kotlin dominates Android, Swift is the path of least resistance for native Apple experiences: typed optionals instead of null-pointer crashes, value-type structs copied by default, and protocol-oriented APIs that favor composition over deep inheritance trees. This guide covers Swift targets and tooling, the type system and optionals, protocols and generics, async/await and actors, SwiftUI versus UIKit, Swift Package Manager, server-side patterns with Vapor, a Harbor Fleet iOS sync client worked example, a language decision table, common pitfalls, and a production checklist alongside our TypeScript and REST API design guides.

What Swift is: platforms, LLVM, and Swift 6

Swift source (.swift files) compiles through LLVM to native machine code on Apple silicon and Intel Macs, with cross-compilation for device simulators and App Store archives. The language is open source (swift.org); Apple ships toolchains via Xcode, while Linux CI runners use the same compiler for server binaries and command-line tools.

Swift 6 introduces stricter complete concurrency checking: types that cross actor boundaries must conform to Sendable, and the compiler flags data races that older code escaped with discipline alone. Migration is gradual — enable SWIFT_STRICT_CONCURRENCY per target and fix warnings before turning errors on in production modules.

Where Swift runs

  • iOS / iPadOS / visionOS — primary consumer surface; App Store distribution and TestFlight beta channels.
  • macOS — menu-bar utilities, pro tools, and Mac App Store or notarized direct downloads.
  • watchOS / tvOS — companion apps and lean interfaces sharing code via multi-target Xcode projects.
  • Linux servers — Vapor, Hummingbird, and AWS Lambda custom runtimes for Swift microservices.
  • Embedded / WASM — experimental; most teams stay on C for hard real-time firmware.

Optionals, value semantics, and the type system

Swift’s headline safety feature is optionals: a value of type String? may be nil, while String cannot. The compiler forces you to unwrap safely with if let, guard let, optional chaining (?), or the nil-coalescing operator (??). Force-unwrapping with ! is explicit and crashes at runtime if wrong — reserve it for tests and provably non-nil Interface Builder outlets.

func displayTrackingId(_ raw: String?) -> String {
    guard let id = raw?.trimmingCharacters(in: .whitespacesAndNewlines),
          !id.isEmpty else {
        return "No tracking ID"
    }
    return id
}

Structs are value types: assignment copies data unless you use class for reference semantics (UIKit views, shared services). Enums are algebraic sum types with associated values — enum SyncResult { case success(Delivery); case conflict(serverRow: Delivery) } replaces stringly-typed status codes. Tuples and typealias keep lightweight groupings readable without boilerplate classes.

Collections and generics

  • Array, Dictionary, Set — copy-on-write collections with value semantics; prefer let for immutable bindings.
  • Genericsfunc deduplicate<T: Hashable>(_ items: [T]) -> [T] shares one implementation across types.
  • Result and throws — errors propagate with throws/try; Result<Success, Failure> suits callback bridges before async migration.

Protocol-oriented programming

Swift favors protocols (interfaces) over class inheritance. Protocol extensions supply default implementations; generic algorithms constrain on protocol requirements instead of base classes. The standard library itself is built this way: Collection, Sequence, and Equatable unlock shared algorithms without a common superclass.

protocol SyncQueueing {
    func enqueue(_ event: SyncEvent) async throws
    var pendingCount: Int { get async }
}

extension SyncQueueing {
    func isEmpty() async -> Bool {
        await pendingCount == 0
    }
}

Existentials (any SyncQueueing) trade some optimization for runtime flexibility; opaque types (some View in SwiftUI) hide concrete return types while preserving compile-time specialization. Choose some in public APIs when the caller does not need to name the underlying type; use any when storing heterogeneous conformers in one array.

Async/await, actors, and structured concurrency

Before Swift 5.5, completion handlers nested deeply and leaked retain cycles. async/await suspends functions without blocking threads; the runtime resumes on cooperative thread pools similar in spirit to Kotlin coroutines. Actors serialize mutable state: only one task mutates an actor’s properties at a time, eliminating lock boilerplate for in-memory queues and caches.

actor OfflineQueue {
    private var events: [SyncEvent] = []

    func append(_ event: SyncEvent) {
        events.append(event)
    }

    func drain() -> [SyncEvent] {
        defer { events.removeAll() }
        return events
    }
}

Use Task for unstructured fire-and-forget work tied to view lifecycles; prefer async let and task groups for parallel independent requests. @MainActor pins UI mutations to the main thread — SwiftUI views and ObservableObject publishers should update UI only from the main actor. Bridge legacy callback APIs with withCheckedContinuation during migration.

SwiftUI, UIKit, and the Apple UI stack

SwiftUI is declarative: you describe view state and the framework diffs the render tree. New greenfield iOS 17+ apps should default to SwiftUI for lists, navigation stacks, and forms. UIKit remains necessary for complex custom transitions, certain camera/AR integrations, and wrapping third-party SDKs that expose view controllers.

  • Observation framework@Observable macros (iOS 17+) replace much ObservableObject boilerplate.
  • Previews — Xcode canvas previews accelerate layout iteration; snapshot tests catch unintended visual regressions.
  • UIViewRepresentable — embed UIKit controls inside SwiftUI when no native wrapper exists.
  • Accessibility — VoiceOver labels, Dynamic Type, and contrast are App Store review expectations, not optional polish.

macOS ships the same SwiftUI APIs with AppKit escape hatches; watchOS and tvOS subsets require leaner view hierarchies and shorter async work before the system suspends your app.

Swift Package Manager and Xcode projects

Swift Package Manager (SPM) is the default dependency manager: Package.swift declares products, targets, and remote or local packages. Xcode integrates SPM natively; CocoaPods and Carthage persist only in legacy codebases.

  • Targets — separate app, test, and shared Core modules; keep networking models in a framework both app and widget extensions import.
  • Platforms clause.iOS(.v17) documents minimum deployment; gate APIs with @available when supporting older devices.
  • Resources — bundle assets via .process("Resources"); localize strings in Localizable.xcstrings.
  • CIxcodebuild test -scheme HarborFleet -destination 'platform=iOS Simulator,name=iPhone 16' on macOS runners; cache DerivedData carefully.

Version-pin dependencies in Package.resolved and commit it for reproducible builds; Renovate or Dependabot can bump security patches on Vapor and Alamofire forks.

Networking and server-side Swift

Client apps typically use URLSession with async wrappers or lightweight libraries built on it. Encode JSON with Codablestruct SyncEvent: Codable, Sendable mirrors OpenAPI schemas shared with Android Kotlin clients. Configure JSONDecoder.dateDecodingStrategy to match server ISO-8601 formats; mismatches here are a top source of silent sync bugs.

On the server, Vapor provides routing, middleware, and Fluent ORM for Postgres on Linux. Swift server adoption is niche compared to Go or Node, but teams already invested in Apple platforms sometimes ship BFF layers in Vapor to share Codable models. For most Harbor-style APIs, the iOS client talks to a Kotlin or Go service; Swift stays on the client unless you have a compelling reason to colocate logic on Linux.

Worked example: Harbor Fleet iOS sync client

Harbor Fleet technicians work offline in warehouses with poor RF coverage. The iOS app must queue delivery status changes locally, upload batches when connectivity returns, and surface server conflicts for human merge.

  1. Shared modelsstruct SyncEvent: Codable, Identifiable, Sendable with clientId: UUID, deliveryId, status, and capturedAt: Date aligned with the Kotlin KMP schema on the server.
  2. Local persistence — SwiftData or Core Data stores pending events; index on clientId for idempotent replays after partial uploads.
  3. Offline queue actor — an OfflineQueue actor appends events from the main UI; a background SyncEngine actor drains when NWPathMonitor reports satisfied connectivity.
  4. Upload — POST /v1/sync with JWT from the fleet auth service; decode 409 conflict payloads into a SwiftUI sheet showing server versus client rows.
  5. Background tasks — register BGAppRefreshTask for opportunistic sync; respect iOS budget limits and never assume unlimited background CPU.
  6. Observability — OSLog signposts for queue depth and upload latency; crash reports via Xcode Organizer; no PII in NSLog strings shipped to production.

Result: compile-time optional handling prevents nil status codes from reaching Core Data, actors isolate queue mutations without locks, and Codable keeps the contract aligned with the JVM sync API documented in our Kotlin guide.

Language decision table

NeedPrefer SwiftConsider instead
New native iOS / iPadOS / visionOS appYes — Apple default and best framework accessReact Native only if web-team velocity outweighs native polish
Cross-platform mobile (iOS + Android)Swift for iOS slice onlyKotlin + KMP or Flutter for shared core
macOS menu-bar or pro utilityYes — SwiftUI + AppKit bridgesElectron if team is web-only (heavier footprint)
Linux microservice at scaleRarely — smaller hiring poolGo, Java, or Node
Browser SPANoTypeScript / React
Game engine (AAA 3D)No for core loopUnity (C#) or Unreal (C++)
watchOS companion with phone appYes — shared Swift modulesStandalone watch app only if scope is tiny
Android-only greenfieldNoKotlin

Common pitfalls

  • Force-unwrapping optionalsvalue! crashes like a null pointer; use guard let early returns in production paths.
  • Retain cycles in closures — capture [weak self] in async callbacks that outlive view controllers; SwiftUI reduces this but network layers still leak.
  • Main-thread blocking — JSON parsing huge payloads on @MainActor freezes UI; offload to background actors.
  • Sendable violations in Swift 6 — passing non-Sendable classes across actors triggers warnings; prefer structs or mark classes @unchecked Sendable only with documented invariants.
  • Date and timezone drift — always store UTC in persistence; format for display with ISO8601DateFormatter matching server contracts.
  • Core Data on main thread — use background contexts for bulk imports; merge changes to the view context on main.
  • App Store entitlement gaps — background modes, push, and keychain access require capability toggles; missing entries fail review mysteriously.
  • Overusing ObservableObject — broad objectWillChange invalidates entire views; prefer granular @Observable or value-type state.

Production checklist

  • Pin Xcode and Swift toolchain versions in CI; document minimum iOS deployment target.
  • Enable Swift 6 concurrency checking incrementally; treat Sendable warnings as release blockers.
  • Unit tests with XCTest; UI tests for offline queue and conflict resolution flows.
  • SwiftLint or SwiftFormat in CI for consistent style across modules.
  • Structured logging with OSLog; privacy-redacted strings in production builds.
  • Crash symbolication uploaded to App Store Connect; monitor hang rate and jetpack metrics.
  • Certificate pinning or ATS exceptions documented; rotate API keys via remote config.
  • TestFlight beta with real warehouse RF conditions before wide rollout.
  • App Store privacy nutrition labels accurate for photo and location usage.
  • Load-test sync batches with duplicate clientId values before peak season.

Key takeaways

  • Swift is the default for native Apple platform apps with optionals, value types, and protocol-oriented APIs.
  • async/await and actors replace callback pyramids; Swift 6 enforces Sendable boundaries across concurrency domains.
  • SwiftUI is the modern UI layer; UIKit remains for integrations SwiftUI does not yet expose cleanly.
  • Choose Swift for iOS/macOS/watchOS greenfield work; pair with Kotlin or Go backends unless you have a specific Vapor-on-Linux mandate.
  • Treat force-unwraps, main-thread blocking, and Codable date mismatches as the primary foot-guns; test offline sync early on real devices.

Related reading