Section 8/131 menit

8. Real Use Cases

8. Real Use Cases

Use Case 1: NetworkLayer Actor

swift
// Production-ready network layer dengan actor isolation

actor NetworkLayer {
    private let session: URLSession
    private var activeTasks: [UUID: Task<Data, Error>] = [:]
    
    init(configuration: URLSessionConfiguration = .default) {
        self.session = URLSession(configuration: configuration)
    }
    
    func fetch(_ request: URLRequest) async throws -> Data {
        let taskID = UUID()
        
        let task = Task<Data, Error> {
            let (data, response) = try await session.data(for: request)
            guard let http = response as? HTTPURLResponse,
                  (200...299).contains(http.statusCode) else {
                throw NetworkError.invalidResponse
            }
            return data
        }
        
        activeTasks[taskID] = task
        defer { activeTasks.removeValue(forKey: taskID) }
        
        return try await task.value
    }
    
    func cancelAll() {
        activeTasks.values.forEach { $0.cancel() }
        activeTasks.removeAll()
    }
    
    var activeRequestCount: Int {
        activeTasks.count
    }
}

enum NetworkError: Error {
    case invalidResponse
}

// Penggunaan
let network = NetworkLayer()

Task {
    let request = URLRequest(url: URL(string: "https://api.example.com/users")!)
    do {
        let data = try await network.fetch(request)
        let users = try JSONDecoder().decode([User].self, from: data)
        await MainActor.run { /* update UI */ }
    } catch {
        print("Error: \(error)")
    }
}

Use Case 2: Thread-Safe In-Memory Cache

swift
actor Cache<Key: Hashable, Value> {
    private var store: [Key: CacheEntry<Value>] = [:]
    private let maxSize: Int
    private let ttl: Duration
    
    struct CacheEntry<V> {
        let value: V
        let expiresAt: ContinuousClock.Instant
    }
    
    init(maxSize: Int = 100, ttl: Duration = .seconds(300)) {
        self.maxSize = maxSize
        self.ttl = ttl
    }
    
    func set(_ value: Value, for key: Key) {
        evictExpiredIfNeeded()
        
        if store.count >= maxSize {
            store.removeFirst()
        }
        
        store[key] = CacheEntry(
            value: value,
            expiresAt: .now + ttl
        )
    }
    
    func get(for key: Key) -> Value? {
        guard let entry = store[key] else { return nil }
        
        if entry.expiresAt < .now {
            store.removeValue(forKey: key)
            return nil
        }
        
        return entry.value
    }
    
    func invalidate(for key: Key) {
        store.removeValue(forKey: key)
    }
    
    func invalidateAll() {
        store.removeAll()
    }
    
    private func evictExpiredIfNeeded() {
        let now = ContinuousClock.now
        store = store.filter { $0.value.expiresAt >= now }
    }
}

// Penggunaan
let userCache = Cache<String, User>(maxSize: 200, ttl: .seconds(600))

Task {
    if let cached = await userCache.get(for: "user_123") {
        print("Cache hit: \(cached.name)")
    } else {
        let user = try await fetchUser(id: "user_123")
        await userCache.set(user, for: "user_123")
    }
}

Use Case 3: UI State Management dengan @MainActor

swift
@MainActor
final class ArticleViewModel: ObservableObject {
    @Published private(set) var articles: [Article] = []
    @Published private(set) var isLoading = false
    @Published private(set) var errorMessage: String?
    
    private let repository: ArticleRepository
    private var loadTask: Task<Void, Never>?
    
    init(repository: ArticleRepository) {
        self.repository = repository
    }
    
    func loadArticles() {
        loadTask?.cancel()
        
        loadTask = Task {
            isLoading = true
            errorMessage = nil
            
            do {
                let fetched = try await repository.fetchAll()
                
                guard !Task.isCancelled else { return }
                
                articles = fetched
            } catch is CancellationError {
                // task dibatalkan — tidak perlu update UI
            } catch {
                errorMessage = error.localizedDescription
            }
            
            isLoading = false
        }
    }
    
    func refresh() async {
        loadTask?.cancel()
        await loadTask?.value
        loadArticles()
    }
    
    deinit {
        loadTask?.cancel()
    }
}

actor ArticleRepository {
    private let networkLayer: NetworkLayer
    private let cache: Cache<String, [Article]>
    
    init() {
        self.networkLayer = NetworkLayer()
        self.cache = Cache(ttl: .seconds(60))
    }
    
    func fetchAll() async throws -> [Article] {
        if let cached = await cache.get(for: "articles") {
            return cached
        }
        
        let request = URLRequest(url: URL(string: "https://api.example.com/articles")!)
        let data = try await networkLayer.fetch(request)
        let articles = try JSONDecoder().decode([Article].self, from: data)
        
        await cache.set(articles, for: "articles")
        return articles
    }
}

struct Article: Codable, Sendable {
    let id: String
    let title: String
    let body: String
}

struct User: Codable, Sendable {
    let id: String
    let name: String
}

Use Case 4: Database Access Actor (Core Data)

swift
@globalActor
actor CoreDataActor {
    static let shared = CoreDataActor()
}

@CoreDataActor
final class CoreDataStack {
    static let shared = CoreDataStack()
    
    private let container: NSPersistentContainer
    
    private init() {
        container = NSPersistentContainer(name: "AppModel")
        container.loadPersistentStores { _, error in
            if let error { fatalError("Core Data failed: \(error)") }
        }
        container.viewContext.automaticallyMergesChangesFromParent = true
    }
    
    var context: NSManagedObjectContext {
        container.viewContext
    }
    
    func performBackgroundTask<T: Sendable>(
        _ block: @CoreDataActor (NSManagedObjectContext) throws -> T
    ) async throws -> T {
        let backgroundContext = container.newBackgroundContext()
        return try await withCheckedThrowingContinuation { continuation in
            backgroundContext.perform {
                do {
                    let result = try block(backgroundContext)
                    continuation.resume(returning: result)
                } catch {
                    continuation.resume(throwing: error)
                }
            }
        }
    }
    
    func save() throws {
        guard context.hasChanges else { return }
        try context.save()
    }
}

@CoreDataActor
class UserRepository {
    private let stack = CoreDataStack.shared
    
    func createUser(name: String, email: String) throws {
        let user = UserEntity(context: stack.context)
        user.id = UUID()
        user.name = name
        user.email = email
        try stack.save()
    }
    
    func fetchUsers() throws -> [UserEntity] {
        let request = UserEntity.fetchRequest()
        request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
        return try stack.context.fetch(request)
    }
}

@MainActor
class UserListViewModel: ObservableObject {
    @Published var users: [UserData] = []
    
    private let repository = UserRepository()
    
    func loadUsers() async {
        do {
            let entities = try await repository.fetchUsers()
            users = entities.map { UserData(id: $0.id!, name: $0.name!, email: $0.email!) }
        } catch {
            print("Failed to load users: \(error)")
        }
    }
}

struct UserData: Sendable {
    let id: UUID
    let name: String
    let email: String
}

class UserEntity: NSManagedObject {
    @NSManaged var id: UUID?
    @NSManaged var name: String?
    @NSManaged var email: String?
    
    static func fetchRequest() -> NSFetchRequest<UserEntity> {
        NSFetchRequest<UserEntity>(entityName: "UserEntity")
    }
}

Use Case 5: Event Bus dengan AsyncStream

swift
actor EventBus<Event: Sendable> {
    private var continuations: [UUID: AsyncStream<Event>.Continuation] = [:]
    
    func subscribe() -> (id: UUID, stream: AsyncStream<Event>) {
        let id = UUID()
        let stream = AsyncStream<Event> { [weak self] continuation in
            Task {
                await self?.register(continuation: continuation, id: id)
            }
            continuation.onTermination = { [weak self, id] _ in
                Task { await self?.unsubscribe(id: id) }
            }
        }
        return (id, stream)
    }
    
    func publish(_ event: Event) {
        continuations.values.forEach { $0.yield(event) }
    }
    
    func unsubscribe(id: UUID) {
        continuations[id]?.finish()
        continuations.removeValue(forKey: id)
    }
    
    private func register(continuation: AsyncStream<Event>.Continuation, id: UUID) {
        continuations[id] = continuation
    }
}

enum AppEvent: Sendable {
    case userLoggedIn(userId: String)
    case userLoggedOut
    case dataRefreshed
    case errorOccurred(message: String)
}

let eventBus = EventBus<AppEvent>()

Task {
    let (_, stream) = await eventBus.subscribe()
    for await event in stream {
        switch event {
        case .userLoggedIn(let userId):
            print("[Analytics] User logged in: \(userId)")
        case .userLoggedOut:
            print("[Analytics] User logged out")
        default:
            break
        }
    }
}

Task { @MainActor in
    let (_, stream) = await eventBus.subscribe()
    for await event in stream {
        switch event {
        case .dataRefreshed:
            print("[UI] Refreshing...")
        case .errorOccurred(let message):
            print("[UI] Error: \(message)")
        default:
            break
        }
    }
}

Task {
    await eventBus.publish(.userLoggedIn(userId: "user_123"))
    try? await Task.sleep(for: .seconds(2))
    await eventBus.publish(.dataRefreshed)
}