Section 15/161 menit

15. Real Use Cases

15. Real Use Cases

Use Case 1: Live Feed dengan Multiple Data Source

Skenario: Social media app yang menampilkan feed real-time dari beberapa sumber — following, trending, dan breaking news — digabung dalam satu timeline dengan rate limiting.

swift
// MARK: - Data Types

struct FeedItem: Identifiable, Hashable {
    let id: UUID
    let source: FeedSource
    let content: String
    let timestamp: Date
}

enum FeedSource: String {
    case following = "Following"
    case trending = "Trending"
    case breakingNews = "Breaking News"
}

// MARK: - Feed Engine

actor FeedEngine {
    private let followingChannel = AsyncChannel<FeedItem>()
    private let trendingChannel = AsyncChannel<FeedItem>()
    private let newsChannel = AsyncChannel<FeedItem>()

    // Simulate data sources (in production: WebSocket, SSE, etc.)
    func startFollowingFeed() async {
        for i in 1... {
            try? await Task.sleep(for: .seconds(Double.random(in: 0.5...3)))
            await followingChannel.send(FeedItem(
                id: UUID(),
                source: .following,
                content: "Post dari following #\(i)",
                timestamp: Date()
            ))
        }
    }

    func startTrendingFeed() async {
        for i in 1... {
            try? await Task.sleep(for: .seconds(Double.random(in: 1...5)))
            await trendingChannel.send(FeedItem(
                id: UUID(),
                source: .trending,
                content: "Trending topic #\(i)",
                timestamp: Date()
            ))
        }
    }

    func startNewsFeed() async {
        for i in 1... {
            try? await Task.sleep(for: .seconds(Double.random(in: 10...30)))
            await newsChannel.send(FeedItem(
                id: UUID(),
                source: .breakingNews,
                content: "Breaking: Berita #\(i)",
                timestamp: Date()
            ))
        }
    }

    // Pipeline utama: merge semua source, throttle untuk performa UI
    func feedStream() -> some AsyncSequence<FeedItem, Never> {
        merge(followingChannel, trendingChannel, newsChannel)
            .removeDuplicates(by: { $0.id == $1.id })
            .throttle(for: .milliseconds(500), reducing: { _, latest in latest })
    }
}

// MARK: - ViewModel

@Observable
@MainActor
final class FeedViewModel {
    var items: [FeedItem] = []
    var sources: Set<FeedSource> = [.following, .trending, .breakingNews]

    private let engine = FeedEngine()
    private var feedTask: Task<Void, Never>?

    func startFeed() {
        // Start data producers
        Task { await engine.startFollowingFeed() }
        Task { await engine.startTrendingFeed() }
        Task { await engine.startNewsFeed() }

        // Consume merged stream
        feedTask = Task { [weak self] in
            guard let self else { return }
            for await item in await engine.feedStream() {
                guard !Task.isCancelled else { break }
                // Prepend untuk tampilan newest-first
                items.insert(item, at: 0)
                // Limit memory: simpan 200 item terbaru
                if items.count > 200 {
                    items = Array(items.prefix(200))
                }
            }
        }
    }

    func stopFeed() {
        feedTask?.cancel()
    }
}

// MARK: - View

struct LiveFeedView: View {
    @State private var viewModel = FeedViewModel()

    var body: some View {
        NavigationStack {
            List(viewModel.items) { item in
                VStack(alignment: .leading, spacing: 4) {
                    HStack {
                        Text(item.source.rawValue)
                            .font(.caption)
                            .padding(4)
                            .background(sourceColor(item.source).opacity(0.2))
                            .clipShape(.capsule)
                        Spacer()
                        Text(item.timestamp, style: .relative)
                            .font(.caption2)
                            .foregroundStyle(.secondary)
                    }
                    Text(item.content)
                }
            }
            .navigationTitle("Live Feed")
            .task {
                viewModel.startFeed()
            }
            .onDisappear {
                viewModel.stopFeed()
            }
        }
    }

    private func sourceColor(_ source: FeedSource) -> Color {
        switch source {
        case .following: return .blue
        case .trending: return .orange
        case .breakingNews: return .red
        }
    }
}

Use Case 2: Analytics Event Pipeline dengan Batching

Skenario: Aplikasi mengirim analytics events ke backend. Events bisa datang sangat cepat (scroll, tap, dll). Perlu di-batch untuk efisiensi: kirim setiap 50 events ATAU setiap 10 detik, mana yang lebih dulu.

swift
// MARK: - Analytics Types

struct AnalyticsEvent: Codable {
    let id: UUID
    let name: String
    let properties: [String: String]
    let sessionID: String
    let timestamp: Date

    init(name: String, properties: [String: String] = [:]) {
        self.id = UUID()
        self.name = name
        self.properties = properties
        self.sessionID = AnalyticsEngine.shared.sessionID
        self.timestamp = Date()
    }
}

// MARK: - Analytics Engine

actor AnalyticsEngine {
    static let shared = AnalyticsEngine()

    let sessionID = UUID().uuidString
    private let eventChannel = AsyncChannel<AnalyticsEvent>()
    private var processorTask: Task<Void, Never>?

    private init() {
        startProcessor()
    }

    // Public API: fire and forget
    nonisolated func track(_ event: AnalyticsEvent) {
        Task {
            await eventChannel.send(event)
        }
    }

    private func startProcessor() {
        processorTask = Task { [weak self] in
            guard let self else { return }

            // Strategi: batch 50 events ATAU 10 detik, gunakan dua stream parallel
            // dan race antara keduanya
            let countBased = eventChannel.chunks(ofCount: 50)
            let timeBased = eventChannel.chunked(by: .repeating(every: .seconds(10)))

            // Gunakan merge untuk ambil batch dari mana saja yang pertama
            // (Dalam praktik: pilih salah satu strategi yang lebih sesuai kebutuhan)
            for await batch in timeBased {
                guard !Task.isCancelled else { break }
                await uploadBatch(Array(batch))
            }
        }
    }

    private func uploadBatch(_ events: [AnalyticsEvent]) async {
        guard !events.isEmpty else { return }
        do {
            let data = try JSONEncoder().encode(events)
            var request = URLRequest(url: URL(string: "https://analytics.example.com/batch")!)
            request.httpMethod = "POST"
            request.httpBody = data
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
            let (_, response) = try await URLSession.shared.data(for: request)
            let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0
            print("Analytics batch: \(events.count) events, status: \(statusCode)")
        } catch {
            print("Analytics upload failed: \(error)")
            // Produksi: implement retry atau persistent queue
        }
    }

    func shutdown() async {
        processorTask?.cancel()
        eventChannel.finish()
    }
}

// MARK: - Penggunaan di View

struct ProductDetailView: View {
    let product: Product

    var body: some View {
        ScrollView {
            VStack { /* content */ }
        }
        .onAppear {
            AnalyticsEngine.shared.track(AnalyticsEvent(
                name: "product_viewed",
                properties: ["product_id": product.id, "category": product.category]
            ))
        }
        .onDisappear {
            AnalyticsEngine.shared.track(AnalyticsEvent(
                name: "product_left",
                properties: ["product_id": product.id]
            ))
        }
    }
}

struct Product { let id: String; let category: String }

Use Case 3: Sensor Fusion dengan Weighted Moving Average

Skenario: Aplikasi kesehatan menerima data dari accelerometer, gyroscope, dan heart rate sensor. Data perlu diproses secara bersama untuk mengklasifikasikan aktivitas.

swift
// MARK: - Sensor Types

struct AccelerometerData { let x, y, z: Double; let timestamp: Date }
struct GyroscopeData { let roll, pitch, yaw: Double; let timestamp: Date }
struct HeartRateData { let bpm: Int; let timestamp: Date }

enum ActivityType: String {
    case resting = "Istirahat"
    case walking = "Berjalan"
    case running = "Berlari"
    case cycling = "Bersepeda"
}

struct ActivityReading {
    let type: ActivityType
    let confidence: Double
    let heartRate: Int
    let timestamp: Date
}

// MARK: - Sensor Fusion Pipeline

@Observable
@MainActor
final class ActivityMonitor {
    var currentActivity: ActivityReading?
    var activityHistory: [ActivityReading] = []

    private var monitoringTask: Task<Void, Never>?

    func startMonitoring(
        accelerometer: some AsyncSequence<AccelerometerData, Never> & Sendable,
        gyroscope: some AsyncSequence<GyroscopeData, Never> & Sendable,
        heartRate: some AsyncSequence<HeartRateData, Never> & Sendable
    ) {
        monitoringTask = Task { [weak self] in
            guard let self else { return }

            // Throttle masing-masing sensor ke 10Hz
            let accel = accelerometer.throttle(
                for: .milliseconds(100),
                reducing: { _, latest in latest }
            )
            let gyro = gyroscope.throttle(
                for: .milliseconds(100),
                reducing: { _, latest in latest }
            )

            // Heart rate lebih jarang — ambil per 5 detik
            let hr = heartRate.throttle(
                for: .seconds(5),
                reducing: { _, latest in latest }
            )

            // combineLatest: emit setiap kali salah satu sensor update
            // (membutuhkan nilai awal dari semua sebelum emit pertama)
            for await (accelData, gyroData) in zip(accel, gyro) {
                guard !Task.isCancelled else { break }

                let activity = classifyActivity(accel: accelData, gyro: gyroData)

                // Smoothing: reductions untuk running average confidence
                let reading = ActivityReading(
                    type: activity.type,
                    confidence: activity.confidence,
                    heartRate: currentActivity?.heartRate ?? 60,
                    timestamp: Date()
                )

                currentActivity = reading
                activityHistory.append(reading)

                if activityHistory.count > 500 {
                    activityHistory.removeFirst()
                }
            }
        }

        // Separate task untuk heart rate
        Task { [weak self] in
            for await hrData in heartRate.throttle(
                for: .seconds(5),
                reducing: { _, latest in latest }
            ) {
                guard let self else { break }
                if var current = currentActivity {
                    currentActivity = ActivityReading(
                        type: current.type,
                        confidence: current.confidence,
                        heartRate: hrData.bpm,
                        timestamp: Date()
                    )
                }
            }
        }
    }

    func stopMonitoring() {
        monitoringTask?.cancel()
    }

    private func classifyActivity(
        accel: AccelerometerData,
        gyro: GyroscopeData
    ) -> (type: ActivityType, confidence: Double) {
        let magnitude = sqrt(accel.x * accel.x + accel.y * accel.y + accel.z * accel.z)
        let rotationMagnitude = sqrt(gyro.roll * gyro.roll + gyro.pitch * gyro.pitch + gyro.yaw * gyro.yaw)

        switch (magnitude, rotationMagnitude) {
        case (0..<0.5, 0..<0.3): return (.resting, 0.9)
        case (0.5..<2.0, 0.3..<1.0): return (.walking, 0.8)
        case (2.0..<5.0, 1.0..<3.0): return (.running, 0.85)
        default: return (.cycling, 0.7)
        }
    }
}

// MARK: - View

struct ActivityView: View {
    @State private var monitor = ActivityMonitor()

    var body: some View {
        VStack(spacing: 20) {
            if let activity = monitor.currentActivity {
                VStack {
                    Text(activity.type.rawValue)
                        .font(.largeTitle.bold())
                    Text("Kepercayaan: \(Int(activity.confidence * 100))%")
                        .foregroundStyle(.secondary)
                    Label("\(activity.heartRate) BPM", systemImage: "heart.fill")
                        .foregroundStyle(.red)
                }
            } else {
                ProgressView("Mendeteksi aktivitas...")
            }

            // Mini history chart
            HStack(alignment: .bottom, spacing: 2) {
                ForEach(monitor.activityHistory.suffix(50)) { reading in
                    Rectangle()
                        .fill(activityColor(reading.type))
                        .frame(width: 4, height: CGFloat(reading.confidence * 40))
                }
            }
            .frame(height: 44)
        }
        .padding()
        .navigationTitle("Aktivitas")
    }

    private func activityColor(_ type: ActivityType) -> Color {
        switch type {
        case .resting: return .blue
        case .walking: return .green
        case .running: return .orange
        case .cycling: return .purple
        }
    }
}

extension ActivityReading: Identifiable {
    var id: Date { timestamp }
}