contentintech
Learn/mobile dev/iOS with Swift
Intermediate~20 min read

iOS with Swift

Build modern iOS apps with Swift 6 and SwiftUI — language fundamentals, declarative views, the @State/@Binding/@Observable property wrappers, navigation, async/await concurrency, networking with URLSession and Codable, forms, and the MVVM architecture.

SwiftUISwiftStateConcurrency

Swift & SwiftUI in 2026

Swift is a fast, type-safe, memory-safe language, and SwiftUI is Apple's declarative UI framework for building apps across iOS, iPadOS, macOS, watchOS, and visionOS. Instead of imperatively mutating views, you describe what the UI should look like for a given state, and the framework diffs and re-renders when that state changes.

Modern Swift 6 enforces strict data-race safety at compile time, and SwiftUI has adopted the Observation framework (the @Observable macro) as the standard way to model reference-type state. This guide covers the language essentials, then builds up to a small networked MVVM app.

Swift Language Basics

Use let for constants and var for variables. Prefer let by default — immutability makes code safer and lets the compiler optimize. Types are inferred but can be annotated explicitly.

let appName = "Contentin"          // inferred String, immutable
var counter: Int = 0               // explicit type, mutable
counter += 1

let pi = 3.14159                   // Double
let isReady = true                 // Bool
let scores: [Int] = [10, 20, 30]   // Array<Int>
let user: [String: Int] = ["age": 30]  // Dictionary

// String interpolation
print("\(appName) has \(scores.count) scores")

Optionals

An optional (T?) either holds a value or is nil. This is Swift's answer to null-pointer bugs: you must explicitly unwrap before use. Use if let / guard let for safe unwrapping and ?? to supply a default.

var nickname: String? = nil

// Optional binding (shorthand: same-name unwrap)
if let nickname { print(nickname) } else { print("no nickname") }

// guard: exit early if nil, keeps happy path flat
func greet(_ name: String?) {
    guard let name else { return }
    print("Hi, \(name)")
}

// Nil-coalescing and optional chaining
let display = nickname ?? "Guest"
let count = nickname?.count ?? 0

Structs, Enums & Protocols

Structs are value types (copied on assignment) and are the default modeling tool in Swift. Enums model a fixed set of cases and can carry associated values. Protocols define capabilities that types conform to — the basis for Swift's protocol-oriented design.

struct Task: Identifiable, Codable {
    let id: UUID
    var title: String
    var isDone: Bool = false
}

enum Priority: String, CaseIterable {
    case low, medium, high
}

enum LoadState {
    case idle
    case loading
    case loaded([Task])
    case failed(Error)     // associated value
}

protocol Repository {
    func fetch() async throws -> [Task]
}

Closures

Closures are self-contained blocks of functionality that can be passed around. Trailing-closure syntax makes higher-order functions read cleanly.

let names = ["Zoe", "Amir", "Kai"]
let sorted = names.sorted { $0 < $1 }        // shorthand args
let upper = names.map { $0.uppercased() }
let short = names.filter { $0.count < 4 }
let total = [1, 2, 3].reduce(0, +)

Declarative Views

A SwiftUI view is a struct conforming to the View protocol with a computed body. Views are cheap, disposable value types — the framework recreates them freely and only re-renders what changed.

import SwiftUI

struct WelcomeView: View {
    var body: some View {
        VStack(spacing: 12) {
            Text("Welcome")
                .font(.largeTitle)
                .fontWeight(.bold)
            Text("Let's build something.")
                .foregroundStyle(.secondary)
            HStack {
                Image(systemName: "swift")
                Text("Swift 6")
            }
        }
        .padding()
    }
}

#Preview { WelcomeView() }

Xcode Previews

The #Preview macro renders your view live in the canvas without launching the simulator. Add several previews with different data or a .preferredColorScheme(.dark) modifier to check states side by side.

State & Property Wrappers

Because views are recreated on every render, ordinary stored properties can't hold mutable UI state. SwiftUI provides property wrappers that give state a durable home the framework manages for you.

WrapperUse for
@StateView-local value owned by this view
@BindingTwo-way reference to state owned elsewhere
@ObservableA reference-type model class (Observation)
@EnvironmentShared values injected down the tree
struct CounterView: View {
    @State private var count = 0

    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment") { count += 1 }
            Toggle("Enabled", isOn: $isOn)   // $ makes a Binding
        }
    }
    @State private var isOn = false
}

// A child view receives a two-way Binding
struct StepperRow: View {
    @Binding var value: Int
    var body: some View {
        Stepper("Value: \(value)", value: $value)
    }
}

The @Observable Model

For shared, reference-type state, the modern @Observable macro replaces the older ObservableObject / @Published pattern. SwiftUI automatically tracks exactly which properties a view reads and re-renders only when those change.

import Observation

@Observable
final class Settings {
    var username = "guest"
    var notifications = true
}

struct ProfileView: View {
    @State private var settings = Settings()   // own the object
    var body: some View {
        Form {
            TextField("Name", text: $settings.username)
            Toggle("Notify", isOn: $settings.notifications)
        }
    }
}

The @Observable macro

No more marking every field with @Published. Every stored property of an @Observable class is tracked automatically, and views subscribe only to the properties they actually read — cutting unnecessary re-renders.

Lists & ForEach

List renders scrolling rows efficiently. Pair it with ForEach over an array of Identifiable data. Swipe actions and deletion are built in.

struct TaskList: View {
    @State private var tasks: [Task] = []

    var body: some View {
        List {
            ForEach(tasks) { task in
                HStack {
                    Image(systemName: task.isDone ? "checkmark.circle.fill" : "circle")
                    Text(task.title)
                }
            }
            .onDelete { tasks.remove(atOffsets: $0) }
        }
    }
}

Navigation

NavigationStack is the modern container for push-style navigation. Use NavigationLink with a value plus a navigationDestination for type-driven, data-based routing.

struct RootView: View {
    let tasks: [Task]
    var body: some View {
        NavigationStack {
            List(tasks) { task in
                NavigationLink(task.title, value: task)
            }
            .navigationTitle("Tasks")
            .navigationDestination(for: Task.self) { task in
                DetailView(task: task)
            }
        }
    }
}

Swift Concurrency: async/await

Swift's structured concurrency replaces nested completion handlers with linear async / await code. Suspension points are explicit, and the compiler enforces that UI updates happen on the main actor. Kick off async work from a view with the .task modifier, which auto-cancels when the view disappears.

func loadNumbers() async throws -> [Int] {
    try await Task.sleep(for: .seconds(1))
    return [1, 2, 3]
}

// Run two calls concurrently with async let
func loadAll() async throws {
    async let a = loadNumbers()
    async let b = loadNumbers()
    let combined = try await a + b
    print(combined)
}

Fetching Data: URLSession + Codable

Decode JSON into Swift types by conforming them to Codable. The async URLSession.data(from:) API returns bytes and a response you can decode directly.

struct Post: Codable, Identifiable {
    let id: Int
    let title: String
    let body: String
}

func fetchPosts() async throws -> [Post] {
    let url = URL(string: "https://api.example.com/posts")!
    let (data, response) = try await URLSession.shared.data(from: url)
    guard let http = response as? HTTPURLResponse,
          http.statusCode == 200 else {
        throw URLError(.badServerResponse)
    }
    return try JSONDecoder().decode([Post].self, from: data)
}

Forms

Form groups inputs into platform-styled sections. Bind each control to state with $.

struct NewTaskForm: View {
    @State private var title = ""
    @State private var priority: Priority = .medium

    var body: some View {
        Form {
            Section("Details") {
                TextField("Title", text: $title)
                Picker("Priority", selection: $priority) {
                    ForEach(Priority.allCases, id: \.self) { p in
                        Text(p.rawValue.capitalized).tag(p)
                    }
                }
            }
            Button("Save") { /* persist */ }
                .disabled(title.isEmpty)
        }
    }
}

MVVM Architecture

Model–View–ViewModel keeps views thin. The view renders state and forwards intents; an @Observable view model holds state and business logic. Marking the view model @MainActor guarantees its mutations run on the main thread — safe for UI.

@MainActor
@Observable
final class PostsViewModel {
    var posts: [Post] = []
    var isLoading = false
    var errorMessage: String?

    func load() async {
        isLoading = true
        defer { isLoading = false }
        do {
            posts = try await fetchPosts()
        } catch {
            errorMessage = error.localizedDescription
        }
    }
}

struct PostsView: View {
    @State private var model = PostsViewModel()

    var body: some View {
        NavigationStack {
            List(model.posts) { Text($0.title) }
                .overlay { if model.isLoading { ProgressView() } }
                .navigationTitle("Posts")
                .task { await model.load() }   // runs on appear
        }
    }
}

SwiftUI vs UIKit

AspectSwiftUIUIKit
ParadigmDeclarativeImperative
Building blockView struct (value)UIView / UIViewController (class)
State to UIAutomatic diffingManual updates
LayoutStacks & modifiersAuto Layout constraints
PreviewsLive #Preview canvasRun in simulator
MaturityModern, evolvingBattle-tested, huge API

New apps should start in SwiftUI. When you need a control SwiftUI doesn't expose yet, wrap a UIKit view with UIViewRepresentable — the two frameworks interoperate cleanly.

Practice Exercises

  1. Build a Task struct that is Identifiable and Codable, then render a List of tasks with a toggle to mark each one done.
  2. Add a NavigationStack with a detail screen, passing the selected task via navigationDestination(for:).
  3. Create an @Observable view model that fetches JSON from a public API using async URLSession and JSONDecoder, exposing loading and error states.
  4. Wire the view model into a view with .task, showing a ProgressView while loading and an error banner on failure.
  5. Build a Form to add a new task, disabling the Save button until the title field is non-empty, and validate with a guard.
  6. Refactor a child row into its own view that receives an @Binding, and verify edits propagate back to the parent's state.

Section navigation