Section 14/151 menit
14. Real Use Cases
14. Real Use Cases
Use Case 1: Smart Document Scanner App
App untuk scan dan ekstrak informasi dari dokumen KTP, NPWP, dan faktur — semua on-device, tidak ada data yang keluar dari device.
swift
import Vision
import NaturalLanguage
import UIKit
@MainActor
class DocumentScannerViewModel: ObservableObject {
@Published var scanResult: ScanResult?
@Published var isProcessing = false
struct ScanResult {
let documentType: DocumentType
let extractedFields: [String: String]
let confidence: Float
let rawText: String
}
enum DocumentType {
case ktp, npwp, invoice, unknown
}
func processDocument(_ image: UIImage) async {
isProcessing = true
defer { isProcessing = false }
guard let cgImage = image.cgImage else { return }
// Step 1: OCR
let textBlocks = await extractText(from: cgImage)
let fullText = textBlocks.joined(separator: "\n")
// Step 2: Classify document type
let docType = classifyDocumentType(fullText)
// Step 3: Extract fields berdasarkan type
let fields = await extractFields(from: fullText, docType: docType)
scanResult = ScanResult(
documentType: docType,
extractedFields: fields,
confidence: 0.85,
rawText: fullText
)
}
private func extractText(from cgImage: CGImage) async -> [String] {
await withCheckedContinuation { continuation in
let request = VNRecognizeTextRequest { req, _ in
let results = req.results?
.compactMap { $0.topCandidates(1).first?.string } ?? []
continuation.resume(returning: results)
}
request.recognitionLevel = .accurate
request.usesLanguageCorrection = true
request.recognitionLanguages = ["id-ID", "en-US"]
try? VNImageRequestHandler(cgImage: cgImage).perform([request])
}
}
private func classifyDocumentType(_ text: String) -> DocumentType {
let lowercased = text.lowercased()
if lowercased.contains("nomor induk kependudukan") ||
lowercased.contains("nik") {
return .ktp
} else if lowercased.contains("npwp") ||
lowercased.contains("nomor pokok wajib pajak") {
return .npwp
} else if lowercased.contains("faktur") ||
lowercased.contains("invoice") ||
lowercased.contains("total") {
return .invoice
}
return .unknown
}
private func extractFields(from text: String, docType: DocumentType) async -> [String: String] {
var fields: [String: String] = [:]
let lines = text.components(separatedBy: .newlines)
switch docType {
case .ktp:
// Pattern matching untuk field KTP
for (index, line) in lines.enumerated() {
let upper = line.uppercased()
if upper.contains("NIK") && index + 1 < lines.count {
let nik = lines[index + 1].filter { $0.isNumber }
if nik.count == 16 { fields["NIK"] = nik }
}
if upper.contains("NAMA") && index + 1 < lines.count {
fields["Nama"] = lines[index + 1].trimmingCharacters(in: .whitespaces)
}
if upper.contains("TEMPAT") && upper.contains("LAHIR") {
// Extract tanggal lahir dari format: Tempat/Tgl Lahir: JAKARTA, 01-01-1990
let components = line.components(separatedBy: ":")
if components.count > 1 { fields["Tempat/Tgl Lahir"] = components[1].trimmingCharacters(in: .whitespaces) }
}
}
case .invoice:
// Ekstrak total, tanggal, nomor invoice
let pattern = #"(TOTAL|Total|total)[\s:Rp.]*(\d[\d.,]+)"#
let regex = try? NSRegularExpression(pattern: pattern)
let range = NSRange(text.startIndex..., in: text)
if let match = regex?.firstMatch(in: text, range: range),
let totalRange = Range(match.range(at: 2), in: text) {
fields["Total"] = "Rp " + String(text[totalRange])
}
default:
break
}
return fields
}
}
Use Case 2: Fitness Rep Counter dengan Body Pose
App fitness yang menghitung repetisi gerakan (squat, push-up) menggunakan Vision body pose:
swift
import Vision
import AVFoundation
import Combine
@MainActor
class FitnessRepCounter: NSObject, ObservableObject {
@Published var repCount = 0
@Published var currentPhase: ExercisePhase = .standing
@Published var jointAngles: [String: Double] = [:]
private let captureSession = AVCaptureSession()
private let videoOutput = AVCaptureVideoDataOutput()
private var sequenceHandler = VNSequenceRequestHandler()
private let analysisQueue = DispatchQueue(label: "com.fitness.pose")
enum ExercisePhase { case standing, squatting, unknown }
enum Exercise { case squat, pushup, lunge }
var exercise: Exercise = .squat
private var previousPhase: ExercisePhase = .unknown
private var isTransitioning = false
func startSession() throws {
captureSession.beginConfiguration()
captureSession.sessionPreset = .high
let camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back)!
let input = try AVCaptureDeviceInput(device: camera)
captureSession.addInput(input)
videoOutput.setSampleBufferDelegate(self, queue: analysisQueue)
videoOutput.alwaysDiscardsLateVideoFrames = true
captureSession.addOutput(videoOutput)
captureSession.commitConfiguration()
captureSession.startRunning()
}
}
extension FitnessRepCounter: AVCaptureVideoDataOutputSampleBufferDelegate {
nonisolated func captureOutput(
_ output: AVCaptureOutput,
didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection
) {
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
let poseRequest = VNDetectHumanBodyPoseRequest()
try? sequenceHandler.perform([poseRequest], on: pixelBuffer, orientation: .up)
guard let observation = poseRequest.results?.first as? VNHumanBodyPoseObservation,
let allPoints = try? observation.recognizedPoints(.all) else { return }
// Hitung sudut knee untuk squat
if let hip = allPoints[.rightHip],
let knee = allPoints[.rightKnee],
let ankle = allPoints[.rightAnkle],
hip.confidence > 0.5, knee.confidence > 0.5, ankle.confidence > 0.5 {
let kneeAngle = calculateAngle(
point1: CGPoint(x: hip.x, y: hip.y),
center: CGPoint(x: knee.x, y: knee.y),
point2: CGPoint(x: ankle.x, y: ankle.y)
)
Task { @MainActor in
self.jointAngles["Knee"] = kneeAngle
self.updateSquatPhase(kneeAngle: kneeAngle)
}
}
}
private func calculateAngle(point1: CGPoint, center: CGPoint, point2: CGPoint) -> Double {
let v1 = CGVector(dx: point1.x - center.x, dy: point1.y - center.y)
let v2 = CGVector(dx: point2.x - center.x, dy: point2.y - center.y)
let dot = v1.dx * v2.dx + v1.dy * v2.dy
let mag = sqrt(v1.dx * v1.dx + v1.dy * v1.dy) * sqrt(v2.dx * v2.dx + v2.dy * v2.dy)
guard mag > 0 else { return 0 }
return acos(max(-1, min(1, dot / mag))) * 180 / .pi
}
@MainActor
private func updateSquatPhase(kneeAngle: Double) {
let newPhase: ExercisePhase = kneeAngle < 100 ? .squatting : .standing
// Hitung rep ketika transisi dari squatting ke standing
if previousPhase == .squatting && newPhase == .standing {
repCount += 1
}
previousPhase = currentPhase
currentPhase = newPhase
}
}