Section 16/172 menit
16. Real Use Cases
16. Real Use Cases
Use Case 1: In-Flight Deduplication Cache
Cache HTTP yang men-dedup request paralel untuk URL yang sama.
swift
actor ImageCache {
private var cached: [URL: UIImage] = [:]
private var inflight: [URL: Task<UIImage, Error>] = [:]
func image(for url: URL) async throws -> UIImage {
if let img = cached[url] { return img }
if let existing = inflight[url] { return try await existing.value }
let task = Task<UIImage, Error> {
let (data, _) = try await URLSession.shared.data(from: url)
guard let img = UIImage(data: data) else {
throw URLError(.cannotDecodeContentData)
}
return img
}
inflight[url] = task
defer { inflight[url] = nil }
let image = try await task.value
cached[url] = image
return image
}
func invalidate(_ url: URL) {
cached.removeValue(forKey: url)
}
}
Keputusan desain:
inflightmelacak request berjalan → dedup tanpa lock manual.defer { inflight[url] = nil }membersihkan slot walau ada throw.- Cache hit tetap atomic karena actor.
Use Case 2: Batched Logger dengan Custom Executor
Logger yang menulis ke file tanpa memblock caller.
swift
@globalActor
actor LogActor {
static let shared = LogActor()
let queue = DispatchSerialQueue(label: "log", qos: .utility)
nonisolated var unownedExecutor: UnownedSerialExecutor {
queue.asUnownedSerialExecutor()
}
}
@LogActor
final class FileLogger {
private var buffer: [String] = []
private let url: URL
private let flushInterval: TimeInterval = 1.0
private var flushTask: Task<Void, Never>?
init(url: URL) {
self.url = url
scheduleFlush()
}
func log(_ message: String) {
buffer.append("\(Date()): \(message)\n")
if buffer.count > 100 { flush() }
}
private func flush() {
guard !buffer.isEmpty else { return }
let lines = buffer.joined()
buffer.removeAll(keepingCapacity: true)
if let data = lines.data(using: .utf8) {
try? data.append(to: url)
}
}
private func scheduleFlush() {
flushTask = Task { [weak self] in
while !Task.isCancelled {
try? await Task.sleep(nanoseconds: UInt64(1_000_000_000))
await self?.flush()
}
}
}
}
extension Data {
func append(to url: URL) throws {
if FileManager.default.fileExists(atPath: url.path) {
let handle = try FileHandle(forWritingTo: url)
try handle.seekToEnd()
try handle.write(contentsOf: self)
try handle.close()
} else {
try write(to: url)
}
}
}
Keputusan desain:
- Custom executor (
DispatchSerialQueue) memastikan I/O konsisten di queue priority.utility. - Buffer batched + timer untuk throughput tinggi tanpa overhead I/O per log call.
logadalah sync (di-isolated ke@LogActor) → caller hop sekali per panggilan, bukan blocking I/O.
Use Case 3: ViewModel SwiftUI dengan Background Computation
ViewModel yang menggabungkan computation berat di background dengan UI update di main.
swift
@MainActor
@Observable
final class SearchViewModel {
var query: String = ""
var results: [SearchResult] = []
var isLoading: Bool = false
private let search: SearchService
private var currentTask: Task<Void, Never>?
init(search: SearchService) {
self.search = search
}
func updateQuery(_ newQuery: String) {
query = newQuery
currentTask?.cancel()
guard !newQuery.isEmpty else {
results = []
return
}
isLoading = true
currentTask = Task {
do {
// search.execute jalan di luar MainActor (heavy)
let found = try await search.execute(query: newQuery)
guard !Task.isCancelled else { return }
// ini sudah di MainActor (closure Task mewarisi isolation)
self.results = found
self.isLoading = false
} catch {
guard !Task.isCancelled else { return }
self.isLoading = false
}
}
}
}
actor SearchService {
func execute(query: String) async throws -> [SearchResult] {
// CPU-bound parsing + I/O — bukan di MainActor
try await Task.sleep(nanoseconds: 200_000_000) // simulate I/O
return [SearchResult(text: "result for \(query)")]
}
}
struct SearchResult: Sendable, Identifiable {
let id = UUID()
let text: String
}
Keputusan desain:
- ViewModel
@MainActor→ property access tanpa hop dari View. - Search heavy di-isolated ke
SearchServiceactor → off-main. currentTask?.cancel()di setiap update → debounce-by-cancellation alami.- Hop ke main hanya sekali saat result siap.
Use Case 4: Database Repository dengan Region-Based Transfer
Repository SwiftData/CoreData yang menerima entity builder dari UI thread.
swift
final class UserBuilder {
var name: String = ""
var email: String = ""
var preferences: [String: Bool] = [:]
}
@globalActor
actor DatabaseActor {
static let shared = DatabaseActor()
}
@DatabaseActor
final class UserRepository {
private var users: [UUID: User] = [:]
func create(from builder: sending UserBuilder) async -> User {
// builder sudah ditransfer; caller tidak bisa akses lagi
let user = User(
id: UUID(),
name: builder.name,
email: builder.email,
preferences: builder.preferences
)
users[user.id] = user
return user
}
}
struct User: Sendable, Identifiable {
let id: UUID
let name: String
let email: String
let preferences: [String: Bool]
}
// Caller di MainActor:
@MainActor
func saveUser(repo: UserRepository) async {
let builder = UserBuilder()
builder.name = "Ari"
builder.email = "ari@example.com"
builder.preferences = ["darkMode": true]
let user = await repo.create(from: builder)
// builder.name = "..." // ❌ Error: 'builder' was transferred
print("Created \(user.id)")
}
Keputusan desain:
UserBuildernon-Sendable (var properties) — ergonomis untuk UI.sendingparameter mengizinkan transfer satu kali ke actor.- Region analysis memastikan caller tidak menyentuh builder setelah transfer.
Use Case 5: Rate-Limited API Client
Client API yang menjamin tidak lebih dari N request/detik, lintas semua caller.
swift
actor RateLimitedClient {
private let limit: Int // max requests per window
private let window: TimeInterval // seconds
private var timestamps: [Date] = []
init(limit: Int, window: TimeInterval) {
self.limit = limit
self.window = window
}
func request<T: Decodable & Sendable>(
_ url: URL,
as type: T.Type
) async throws -> T {
try await waitForSlot()
timestamps.append(.now)
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(T.self, from: data)
}
private func waitForSlot() async throws {
while true {
let cutoff = Date().addingTimeInterval(-window)
timestamps.removeAll { $0 < cutoff }
if timestamps.count < limit { return }
// Tunggu hingga slot tertua kadaluarsa
guard let oldest = timestamps.first else { return }
let sleepFor = oldest.addingTimeInterval(window).timeIntervalSinceNow
if sleepFor > 0 {
try await Task.sleep(nanoseconds: UInt64(sleepFor * 1_000_000_000))
}
}
}
}
Keputusan desain:
- State
timestampsaman karena actor. - Re-entrancy aman di sini: kalau Caller A
await sleep, Caller B masuk dan ikut antri sesuai cutoff yang baru. T: Sendabledi generic constraint memastikan hasil bisa di-return cross-isolation.