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)
}