Lesson 99Advanced Topics

Advanced Practice

Putting It All Together

These exercises combine multiple advanced concepts. Practice building real-world solutions using generics, actors, property wrappers, and more!

Practice Projects

1. Generic Cache with Actor

Thread-safe cache using generics and actors.

2. Async Network Layer

Protocol-based API client with async/await.

3. Property Wrapper + Codable

Custom wrapper for safe JSON decoding.

4. Result Builder

DSL for building arrays declaratively.

5. Generic Repository

Protocol with associated types for data access.

6. Type-Safe State Machine

Generics for compile-time state validation.

Concepts Combined

  • Generics with constraints
  • Actors for thread safety
  • Async/await patterns
  • Property wrappers
  • Protocols with associated types
  • Result builders
advanced_practice.swift
// ADVANCED PRACTICE
// Comprehensive exercises combining advanced concepts

// ===== EXERCISE 1: Generic Cache with Actor =====
actor Cache<Key: Hashable, Value> {
    private var storage: [Key: Value] = [:]
    private let maxSize: Int

    init(maxSize: Int = 100) {
        self.maxSize = maxSize
    }

    func get(_ key: Key) -> Value? {
        return storage[key]
    }

    func set(_ key: Key, value: Value) {
        if storage.count >= maxSize {
            storage.removeAll()  // Simple eviction
        }
        storage[key] = value
    }

    func clear() {
        storage.removeAll()
    }
}

// Usage
let imageCache = Cache<String, Data>(maxSize: 50)

Task {
    await imageCache.set("avatar", value: Data())
    let data = await imageCache.get("avatar")
}

// ===== EXERCISE 2: Async Network Layer =====
protocol APIClient {
    func fetch<T: Decodable>(from url: URL) async throws -> T
}

actor NetworkManager: APIClient {
    func fetch<T: Decodable>(from url: URL) async throws -> T {
        let (data, response) = try await URLSession.shared.data(from: url)

        guard let httpResponse = response as? HTTPURLResponse,
              (200...299).contains(httpResponse.statusCode) else {
            throw NetworkError.invalidResponse
        }

        return try JSONDecoder().decode(T.self, from: data)
    }
}

enum NetworkError: Error {
    case invalidResponse
    case decodingFailed
}

// ===== EXERCISE 3: Property Wrapper + Codable =====
@propertyWrapper
struct DefaultEmpty<T: RangeReplaceableCollection & Codable>: Codable {
    var wrappedValue: T

    init(wrappedValue: T) {
        self.wrappedValue = wrappedValue
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        wrappedValue = (try? container.decode(T.self)) ?? T()
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(wrappedValue)
    }
}

struct User: Codable {
    var name: String
    @DefaultEmpty var tags: [String]  // Won't fail if missing
}

// ===== EXERCISE 4: Result Builder =====
@resultBuilder
struct ArrayBuilder<T> {
    static func buildBlock(_ components: T...) -> [T] {
        components
    }

    static func buildOptional(_ component: [T]?) -> [T] {
        component ?? []
    }

    static func buildEither(first: [T]) -> [T] { first }
    static func buildEither(second: [T]) -> [T] { second }
}

func buildArray<T>(@ArrayBuilder<T> _ content: () -> [T]) -> [T] {
    content()
}

let numbers = buildArray {
    1
    2
    3
}
print(numbers)  // [1, 2, 3]

// ===== EXERCISE 5: Combine Generics + Protocols =====
protocol Repository {
    associatedtype Entity: Identifiable
    func fetch(id: Entity.ID) async throws -> Entity
    func save(_ entity: Entity) async throws
    func delete(id: Entity.ID) async throws
}

struct InMemoryRepository<T: Identifiable>: Repository {
    typealias Entity = T
    private var storage: [T.ID: T] = [:]

    func fetch(id: T.ID) async throws -> T {
        guard let entity = storage[id] else {
            throw RepositoryError.notFound
        }
        return entity
    }

    mutating func save(_ entity: T) async throws {
        storage[entity.id] = entity
    }

    mutating func delete(id: T.ID) async throws {
        storage.removeValue(forKey: id)
    }
}

enum RepositoryError: Error {
    case notFound
    case saveFailed
}

// ===== EXERCISE 6: Type-Safe State Machine =====
protocol State {}
protocol Event {}

struct StateMachine<S: State, E: Event> {
    private(set) var currentState: S
    private let transitions: (S, E) -> S?

    init(initial: S, transitions: @escaping (S, E) -> S?) {
        self.currentState = initial
        self.transitions = transitions
    }

    mutating func send(_ event: E) -> Bool {
        guard let newState = transitions(currentState, event) else {
            return false
        }
        currentState = newState
        return true
    }
}

// Example usage
enum OrderState: State {
    case pending, processing, shipped, delivered
}

enum OrderEvent: Event {
    case pay, ship, deliver
}

var orderMachine = StateMachine<OrderState, OrderEvent>(
    initial: .pending
) { state, event in
    switch (state, event) {
    case (.pending, .pay): return .processing
    case (.processing, .ship): return .shipped
    case (.shipped, .deliver): return .delivered
    default: return nil
    }
}

orderMachine.send(.pay)      // Now .processing
orderMachine.send(.ship)     // Now .shipped
orderMachine.send(.deliver)  // Now .delivered

Challenge Yourself!

Combine multiple exercises: Create an async repository that uses the actor-based cache for performance!