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