Section 16/172 menit
16. Real Use Cases
16. Real Use Cases
Use Case 1: Notes App dengan AI Search (Hybrid)
App notes dengan semantic search lokal + AI summarization opsional.
swift
// notes-ai: Architecture
@MainActor
@Observable
final class NotesVM {
var notes: [Note] = []
var searchResults: [NoteSearchResult] = []
var isSearching = false
private let store: NotesStore
private let vectorStore: VectorStore
private let ai: AIService
init(store: NotesStore, vectorStore: VectorStore, ai: AIService) {
self.store = store
self.vectorStore = vectorStore
self.ai = ai
}
func semanticSearch(_ query: String) async {
isSearching = true
defer { isSearching = false }
do {
let chunks = try await vectorStore.search(query: query, topK: 10)
let noteIDs = Set(chunks.map(\.documentID))
let foundNotes = try await store.notes(ids: Array(noteIDs))
searchResults = foundNotes.map { note in
NoteSearchResult(
note: note,
relevantSnippet: chunks.first { $0.documentID == note.id }?.content ?? ""
)
}
} catch {
searchResults = []
}
}
func summarizeNote(_ note: Note) async throws -> String {
return try await ai.summarize(note.content)
}
func saveNote(_ note: Note) async throws {
try await store.save(note)
try await vectorStore.index(documentID: note.id, content: note.content)
}
}
struct NoteSearchResult: Identifiable {
let id = UUID()
let note: Note
let relevantSnippet: String
}
Keputusan desain:
vectorStoreuntuk semantic search — selalu on-device (privat).ai.summarizebisa on-device (Foundation Models) atau cloud — abstracted via protocol.- Index dijalankan saat save, bukan saat search (eager indexing).
Use Case 2: Receipt Scanner (Vision + LLM)
Scan struk → extract structured data → simpan ke database.
swift
// receipt-scanner: Vision OCR + Foundation Models extraction
import Vision
import FoundationModels
@Generable
struct ReceiptData: Sendable {
let merchant: String
let date: Date
let totalAmount: Double
let currency: String
let items: [Item]
@Generable
struct Item: Sendable {
let name: String
let quantity: Int
let price: Double
}
}
actor ReceiptScanner {
func scan(_ image: CGImage) async throws -> ReceiptData {
// Step 1: OCR dengan Vision
let request = VNRecognizeTextRequest()
request.recognitionLevel = .accurate
request.recognitionLanguages = ["id-ID", "en-US"]
let handler = VNImageRequestHandler(cgImage: image)
try handler.perform([request])
let observations = request.results ?? []
let text = observations.compactMap { $0.topCandidates(1).first?.string }
.joined(separator: "\n")
// Step 2: Extract structured data dengan Foundation Models
let session = LanguageModelSession()
return try await session.respond(
to: """
Ekstrak data dari struk berikut. Kalau tanggal/total tidak jelas, pilih yang paling mungkin.
\(text)
""",
generating: ReceiptData.self
)
}
}
@MainActor
@Observable
final class ScanReceiptVM {
var status: Status = .idle
var receipt: ReceiptData?
private let scanner: ReceiptScanner
init(scanner: ReceiptScanner) { self.scanner = scanner }
func scan(_ image: CGImage) async {
status = .scanning
do {
let result = try await scanner.scan(image)
self.receipt = result
status = .done
} catch {
status = .error(error.localizedDescription)
}
}
enum Status {
case idle, scanning, done
case error(String)
}
}
Keputusan desain:
- Vision untuk OCR (akurat untuk teks struk, gratis, offline).
- Foundation Models untuk parsing tidak terstruktur → struktur.
@Generablemembuat output type-safe. - Semua on-device — data finansial user tidak keluar.
- Status enum untuk UI state.
Use Case 3: Voice Assistant in-App dengan Cloud LLM
Chat suara — speech-to-text on-device, LLM cloud, TTS on-device.
swift
// voice-assistant: Pipeline lengkap
import Speech
import AVFoundation
@MainActor
@Observable
final class VoiceAssistantVM {
var transcript: String = ""
var aiResponse: String = ""
var isListening = false
var isSpeaking = false
private let recognizer = SFSpeechRecognizer(locale: Locale(identifier: "id-ID"))!
private let audioEngine = AVAudioEngine()
private let synthesizer = AVSpeechSynthesizer()
private let ai: AIService
private var recognitionTask: SFSpeechRecognitionTask?
init(ai: AIService) { self.ai = ai }
func startListening() throws {
isListening = true
transcript = ""
let request = SFSpeechAudioBufferRecognitionRequest()
request.shouldReportPartialResults = true
request.requiresOnDeviceRecognition = true // privacy
let inputNode = audioEngine.inputNode
let format = inputNode.outputFormat(forBus: 0)
inputNode.installTap(onBus: 0, bufferSize: 1024, format: format) { buffer, _ in
request.append(buffer)
}
try audioEngine.start()
recognitionTask = recognizer.recognitionTask(with: request) { [weak self] result, error in
guard let self else { return }
if let result {
Task { @MainActor in
self.transcript = result.bestTranscription.formattedString
if result.isFinal {
await self.processQuery(self.transcript)
}
}
}
}
}
func stopListening() {
audioEngine.stop()
audioEngine.inputNode.removeTap(onBus: 0)
recognitionTask?.finish()
isListening = false
}
private func processQuery(_ query: String) async {
do {
aiResponse = ""
for try await chunk in try await ai.stream(prompt: query) {
aiResponse += chunk
}
speak(aiResponse)
} catch {
aiResponse = "Maaf, terjadi error: \(error.localizedDescription)"
}
}
private func speak(_ text: String) {
isSpeaking = true
let utterance = AVSpeechUtterance(string: text)
utterance.voice = AVSpeechSynthesisVoice(language: "id-ID")
utterance.rate = 0.5
synthesizer.speak(utterance)
}
}
Keputusan desain:
- Speech-to-text
requiresOnDeviceRecognition: trueuntuk privasi. - LLM cloud untuk capability tinggi (reasoning, knowledge).
- TTS lokal lewat AVSpeechSynthesizer.
- Streaming response — user dengar mulai jawaban sebelum semuanya selesai.
Use Case 4: Smart Compose Email dengan Privacy-Aware Fallback
Auto-suggest text completion: gunakan Foundation Models jika tersedia, fallback ke local Markov-based suggestion jika tidak.
swift
// smart-compose: Privacy-first, multi-tier
protocol TextSuggester: Sendable {
func suggest(prefix: String, context: String) async throws -> String?
}
actor FoundationModelsSuggester: TextSuggester {
private let session = LanguageModelSession()
func suggest(prefix: String, context: String) async throws -> String? {
let prompt = """
Lanjutkan teks berikut secara natural, hanya 5-15 kata. Tanpa penjelasan.
Konteks: \(context)
Awal: \(prefix)
Lanjutan:
"""
let response = try await session.respond(to: prompt)
return response.content
}
}
actor LocalMarkovSuggester: TextSuggester {
private var transitions: [String: [String]] = [:]
func train(with corpus: [String]) {
for text in corpus {
let tokens = text.split(separator: " ").map(String.init)
for i in 0..<tokens.count - 1 {
transitions[tokens[i], default: []].append(tokens[i + 1])
}
}
}
func suggest(prefix: String, context: String) async throws -> String? {
let tokens = prefix.split(separator: " ").map(String.init)
guard let last = tokens.last,
let candidates = transitions[last]?.shuffled(),
let next = candidates.first else { return nil }
return next
}
}
@MainActor
@Observable
final class ComposeEmailVM {
var draft: String = ""
var suggestion: String?
private let suggester: TextSuggester
private var suggestTask: Task<Void, Never>?
init() {
if SystemModel.isAppleIntelligenceAvailable {
self.suggester = FoundationModelsSuggester()
} else {
self.suggester = LocalMarkovSuggester()
}
}
func didChangeDraft(_ newDraft: String) {
draft = newDraft
suggestTask?.cancel()
suggestTask = Task { [weak self] in
try? await Task.sleep(for: .milliseconds(400))
guard !Task.isCancelled, let self else { return }
do {
let s = try await suggester.suggest(prefix: draft, context: "email")
guard !Task.isCancelled else { return }
self.suggestion = s
} catch {
self.suggestion = nil
}
}
}
func acceptSuggestion() {
guard let suggestion else { return }
draft += " " + suggestion
self.suggestion = nil
}
}
Keputusan desain:
- Protocol
TextSuggester→ swappable implementasi. - Foundation Models untuk device support, Markov local untuk device lama → semua user dapat fitur.
- Debounce 400ms via Task cancel pattern.
- Tidak ada cloud — privasi total untuk email content.