Skip to content
Swift Advanced: Protocols, Generics & Concurrency

Swift Advanced: Protocols, Generics & Concurrency

DodaTech Updated Jun 20, 2026 8 min read

Swift’s protocol-oriented programming, advanced generics, and structured concurrency (async/await, actors) make it one of the most expressive modern languages. These features enable type-safe, performant, and data-race-free code.

In this tutorial, you’ll master protocol-oriented programming with associated types, opaque types using some, Result builders for DSLs, async/await internals, actors for safe mutable state, and the Sendable protocol.

What You’ll Learn

  • Protocol-oriented programming: protocol extensions, default implementations
  • Associated types and generic constraints
  • Opaque types (some) vs concrete types
  • Result builders for DSL construction
  • Async/await: Continuation, Task, TaskGroup
  • Actors: isolation, reentrancy, MainActor
  • Sendable protocol and data-race safety

Why Advanced Swift Matters

Swift is the language for Apple ecosystem development. Advanced Swift features power SwiftUI, the most popular UI framework on Apple platforms. At DodaTech, Doda Browser’s macOS frontend uses actors for safe shared state and Result builders for declarative UI.

Learning Path

    flowchart LR
  A[Swift Basics] --> B[Swift Advanced<br/>You are here]
  B --> C[Actors & Concurrency]
  B --> D[SwiftUI Internals]
  style B fill:#f90,color:#fff
  

Protocol-Oriented Programming

Protocols with extensions provide default implementations — like traits in Rust:

protocol Vehicle {
    var name: String { get }
    var currentSpeed: Double { get set }
    func travel(to destination: String) -> String
}

// Protocol extension — default implementation
extension Vehicle {
    func travel(to destination: String) -> String {
        return "\(name) is traveling to \(destination)"
    }

    func description() -> String {
        return "\(name) at speed \(currentSpeed)"
    }
}

struct Car: Vehicle {
    let name: String
    var currentSpeed: Double
    var fuelLevel: Double
}

struct Bicycle: Vehicle {
    let name: String
    var currentSpeed: Double
}

let car = Car(name: "Tesla", currentSpeed: 0, fuelLevel: 0.8)
print(car.travel(to: "New York"))
// Tesla is traveling to New York
print(car.description())
// Tesla at speed 0.0

Expected output: Protocol extensions provide default implementations. Types conforming to Vehicle get travel and description automatically unless they provide their own.

Associated Types and Generics

Associated types add type parameters to protocols:

protocol Container {
    associatedtype Item
    var count: Int { get }
    mutating func append(_ item: Item)
    subscript(i: Int) -> Item { get }
}

// Generic constraint with where clause
struct Stack<Element>: Container {
    private var items: [Element] = []

    var count: Int { items.count }

    mutating func push(_ item: Element) {
        items.append(item)
    }

    mutating func pop() -> Element? {
        return items.popLast()
    }

    // Container conformance
    mutating func append(_ item: Element) {
        push(item)
    }

    subscript(i: Int) -> Element {
        return items[i]
    }
}

// Type constraint with where
func allItemsMatch<C1: Container, C2: Container>(
    _ container1: C1,
    _ container2: C2
) -> Bool where C1.Item == C2.Item, C1.Item: Equatable {
    guard container1.count == container2.count else { return false }
    for i in 0..<container1.count {
        if container1[i] != container2[i] { return false }
    }
    return true
}

var stack1 = Stack<String>()
stack1.push("a")
stack1.push("b")

var stack2 = Stack<String>()
stack2.push("a")
stack2.push("b")

print(allItemsMatch(stack1, stack2)) // true

Opaque Types (some)

some hides the concrete type while preserving type identity:

// Opaque return type — compiler knows the type, caller doesn't
func makeVehicle() -> some Vehicle {
    return Car(name: "Default", currentSpeed: 0, fuelLevel: 1.0)
}

// The caller can use Vehicle methods but doesn't know the concrete type
let vehicle = makeVehicle()
print(vehicle.description())

// Opaque types are important for SwiftUI:
// var body: some View { ... }
// The compiler knows the exact View type; the developer doesn't need to

// Difference between some and any:
// - "some" = opaque (fixed concrete type)
// - "any" = existential (boxed, dynamic dispatch)
func processVehicle(_ v: any Vehicle) { }
processVehicle(vehicle)

Expected behavior: some Vehicle guarantees the same concrete type is always returned. The compiler resolves it statically. any Vehicle uses dynamic dispatch via a box.

Result Builders

Result builders transform sequences of statements into a single value:

@resultBuilder
struct StringBuilder {
    static func buildBlock(_ components: String...) -> String {
        return components.joined(separator: " ")
    }

    static func buildOptional(_ component: String?) -> String {
        return component ?? ""
    }

    static func buildEither(first component: String) -> String {
        return component
    }

    static func buildEither(second component: String) -> String {
        return component
    }

    static func buildArray(_ components: [String]) -> String {
        return components.joined(separator: ", ")
    }
}

func build(@StringBuilder _ content: () -> String) -> String {
    return content()
}

let greeting = build {
    "Hello"
    "Swift"
    if true {
        "Advanced"
    }
}
print(greeting) // "Hello Swift Advanced"

Expected output: The result builder collects all statements, joins them with spaces, and conditionally includes the “Advanced” string.

Async/Await Internals

Swift’s async/await uses a runtime-managed continuation model:

import Foundation

// Suspending function
func fetchUser(id: Int) async throws -> User {
    let url = URL(string: "https://api.example.com/users/\(id)")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(User.self, from: data)
}

// TaskGroup for structured concurrency
func fetchAllUsers(ids: [Int]) async throws -> [User] {
    return try await withThrowingTaskGroup(of: User.self) { group in
        for id in ids {
            group.addTask {
                return try await fetchUser(id: id)
            }
        }

        var results: [User] = []
        for try await user in group {
            results.append(user)
        }
        return results
    }
}

// Continuation — bridge callback APIs
func fetchUserName(id: Int) async -> String {
    return await withCheckedContinuation { continuation in
        // Simulate callback-based API
        DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
            continuation.resume(returning: "User \(id)")
        }
    }
}

Expected behavior: fetchAllUsers fetches all users concurrently using a task group. withCheckedContinuation bridges callback-based APIs to async/await.

Actors

Actors protect mutable state from data races:

actor BankAccount {
    private var balance: Double

    init(initialBalance: Double) {
        self.balance = initialBalance
    }

    func deposit(amount: Double) {
        balance += amount
    }

    func withdraw(amount: Double) -> Bool {
        guard balance >= amount else { return false }
        balance -= amount
        return true
    }

    func getBalance() -> Double {
        balance
    }
}

// MainActor — runs on the main thread
@MainActor
class ViewModel: ObservableObject {
    @Published var balance: Double = 0

    func updateBalance(from account: BankAccount) async {
        let newBalance = await account.getBalance()
        self.balance = newBalance // Must be on MainActor
    }
}

// Usage
let account = BankAccount(initialBalance: 1000)
await account.deposit(amount: 500)
let balance = await account.getBalance()
print(balance) // 1500

Expected behavior: All access to BankAccount’s state goes through the actor’s serial executor. Data races are impossible — the compiler enforces actor isolation.

Sendable

Sendable marks types safe to pass across concurrency domains:

// Value types are implicitly Sendable
struct User: Sendable {
    let id: Int
    let name: String
}

// Classes must be explicitly Sendable and final
final class Configuration: @unchecked Sendable {
    let timeout: Int
    let retries: Int

    init(timeout: Int, retries: Int) {
        self.timeout = timeout
        self.retries = retries
    }
}

// @unchecked is needed for classes with mutable state
// handled with locks or serial queues

Common Mistakes

1. Using any When some Would Work

any Vehicle boxes the value (heap allocation, dynamic dispatch). Use some Vehicle when you have a single concrete type.

2. Actors Calling Synchronous Blocking Code

Actor methods should be async. Calling synchronous blocking code inside an actor blocks its executor, blocking all other calls to that actor.

3. Reference Cycles with Closures

Closures capture self strongly. Use [weak self] in capture lists to prevent retain cycles.

4. Not Using Result Builders for Composition

Result builders compose multiple children without nested brackets. They’re essential for SwiftUI’s VStack { ... } syntax.

5. Ignoring MainActor for UI Updates

Accessing UIKit/SwiftUI properties from background threads crashes. Mark UI-bound methods with @MainActor.

6. Using Classes Instead of Structs for Data Models

Value semantics prevent accidental shared state. Default to struct; use class only when you need reference semantics or inheritance.

Practice Questions

1. What is protocol-oriented programming?

Designing systems around protocols with default implementations in extensions rather than class inheritance hierarchies.

2. What does the some keyword mean?

It declares an opaque type — the caller sees a protocol but the concrete type is fixed at compile time.

3. What is an actor?

A concurrency primitive that protects its mutable state from data races by serializing access through an executor.

4. What is a Result Builder?

A type annotated with @resultBuilder that converts a sequence of statements into a single composite value.

5. Challenge: Build a simple Result Builder for HTML.

Create an HTMLBuilder that takes div, p, and h1 statements and produces an HTML string.

Mini Project: Safe Bank Account

actor SafeAccount {
    private var balance: Double
    private var transactionLog: [String] = []

    init(initial: Double) {
        self.balance = initial
    }

    func transfer(amount: Double, to account: isolated SafeAccount) async {
        guard balance >= amount else { return }
        balance -= amount
        await account.deposit(amount: amount)
        transactionLog.append("Transferred \(amount)")
    }

    func deposit(amount: Double) {
        balance += amount
        transactionLog.append("Deposited \(amount)")
    }
}

FAQ

What’s the difference between actors and locks?
Actors are compiler-enforced — the compiler prevents data races at compile time. Locks are runtime mechanisms with no compile-time protection.
Can I use async/await without a completion handler?
Yes. Swift’s async/await eliminates completion handlers. Use withCheckedContinuation to bridge callback APIs.
What is @MainActor?
A global actor that executes all its functions on the main thread. Mark view model methods with @MainActor for thread-safe UI updates.

What’s Next

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro