Section 10/161 menit

10. Integrasi dengan SwiftUI

10. Integrasi dengan SwiftUI

@Observable + AsyncChannel

swift
// ViewModel dengan async pipeline
import AsyncAlgorithms
import Observation

@Observable
final class SearchViewModel {
    var results: [SearchResult] = []
    var isLoading = false
    var errorMessage: String?

    private let queryChannel = AsyncChannel<String>()
    private var searchTask: Task<Void, Never>?

    init() {
        startPipeline()
    }

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

            // Pipeline: debounce → deduplicate → search
            let pipeline = queryChannel
                .debounce(for: .milliseconds(300))
                .removeDuplicates()

            for await query in pipeline {
                guard !Task.isCancelled else { break }
                await performSearch(query: query)
            }
        }
    }

    func search(_ query: String) async {
        await queryChannel.send(query)
    }

    @MainActor
    private func performSearch(query: String) async {
        guard !query.isEmpty else {
            results = []
            return
        }

        isLoading = true
        do {
            results = try await SearchAPI.search(query: query)
            errorMessage = nil
        } catch {
            errorMessage = error.localizedDescription
        }
        isLoading = false
    }

    deinit {
        searchTask?.cancel()
        queryChannel.finish()
    }
}

// View
struct SearchView: View {
    @State private var viewModel = SearchViewModel()
    @State private var query = ""

    var body: some View {
        NavigationStack {
            List(viewModel.results) { result in
                SearchResultRow(result: result)
            }
            .overlay {
                if viewModel.isLoading {
                    ProgressView()
                } else if let error = viewModel.errorMessage {
                    ContentUnavailableView("Error", systemImage: "exclamationmark.triangle",
                                          description: Text(error))
                }
            }
            .searchable(text: $query)
            .onChange(of: query) { _, newValue in
                Task { await viewModel.search(newValue) }
            }
            .navigationTitle("Cari")
        }
    }
}

AsyncSequence ke SwiftUI via AsyncStream Bridge

Untuk feed data realtime ke SwiftUI dari async sequence:

swift
// Bridge AsyncSequence ke @Published / Binding
@Observable
final class LiveDataViewModel {
    var latestValue: Double = 0
    var history: [Double] = []

    private var streamTask: Task<Void, Never>?

    func startMonitoring(stream: some AsyncSequence<Double, Never> & Sendable) {
        streamTask = Task { @MainActor in
            // throttle untuk tidak update UI terlalu sering
            for await value in stream.throttle(for: .milliseconds(100), reducing: { _, v in v }) {
                latestValue = value
                history.append(value)
                if history.count > 100 {
                    history.removeFirst()
                }
            }
        }
    }

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