Section 17/181 menit

17. Real Use Cases

17. Real Use Cases

Use Case 1: Task Manager dengan Full App Intents Integration

Implementasi lengkap task manager dengan Siri, Shortcuts, dan Interactive Widget.

swift
import AppIntents
import WidgetKit

// --- ENTITIES ---

struct TaskEntity: AppEntity, Identifiable {
    var id: UUID
    var title: String
    var isCompleted: Bool
    var priority: PriorityLevel
    var dueDate: Date?
    
    static var typeDisplayRepresentation: TypeDisplayRepresentation = TypeDisplayRepresentation(
        name: "Tugas",
        numericFormat: "\(placeholder: .count) tugas"
    )
    
    var displayRepresentation: DisplayRepresentation {
        DisplayRepresentation(
            title: "\(title)",
            subtitle: "\(priority.displayRepresentation.title ?? "Sedang") · \(isCompleted ? "Selesai" : "Belum")",
            image: .init(systemName: isCompleted ? "checkmark.circle.fill" : "circle")
        )
    }
    
    static var defaultQuery = TaskEntityQuery()
}

struct TaskEntityQuery: EntityQuery, EntityStringQuery {
    func entities(for identifiers: [UUID]) async throws -> [TaskEntity] {
        try await TaskRepository.shared.fetch(ids: identifiers).map(TaskEntity.init(task:))
    }
    
    func suggestedEntities() async throws -> [TaskEntity] {
        try await TaskRepository.shared.fetchPending(limit: 15).map(TaskEntity.init(task:))
    }
    
    func entities(matching string: String) async throws -> [TaskEntity] {
        try await TaskRepository.shared.search(query: string).map(TaskEntity.init(task:))
    }
}

extension TaskEntity {
    init(task: Task) {
        self.id = task.id
        self.title = task.title
        self.isCompleted = task.isCompleted
        self.priority = task.priority
        self.dueDate = task.dueDate
    }
}

// --- INTENTS ---

struct AddTaskIntent: AppIntent {
    static var title: LocalizedStringResource = "Tambah Tugas"
    static var description = IntentDescription("Tambahkan tugas baru ke daftar kamu")
    
    @Parameter(title: "Judul", requestValueDialog: "Apa judul tugasnya?")
    var title: String
    
    @Parameter(title: "Prioritas", default: .medium)
    var priority: PriorityLevel
    
    @Parameter(title: "Tenggat")
    var dueDate: Date?
    
    func perform() async throws -> some IntentResult & ReturnsValue<TaskEntity> & ProvidesDialog {
        let task = try await TaskRepository.shared.create(title: title, priority: priority, dueDate: dueDate)
        await AppShortcuts.updateAppShortcutParameters()
        
        return .result(
            value: TaskEntity(task: task),
            dialog: "Tugas '\(title)' sudah ditambahkan\(dueDate != nil ? " dengan tenggat \(formatted(dueDate!))" : "")."
        )
    }
}

struct CompleteTaskIntent: AppIntent {
    static var title: LocalizedStringResource = "Selesaikan Tugas"
    
    @Parameter(title: "Tugas", requestValueDialog: "Tugas mana yang ingin diselesaikan?")
    var task: TaskEntity
    
    func perform() async throws -> some IntentResult & ProvidesDialog {
        try await TaskRepository.shared.markCompleted(id: task.id)
        await AppShortcuts.updateAppShortcutParameters()
        return .result(dialog: "'\(task.title)' sudah selesai! Kerja bagus!")
    }
}

struct GetPendingCountIntent: AppIntent {
    static var title: LocalizedStringResource = "Jumlah Tugas Pending"
    
    func perform() async throws -> some IntentResult & ReturnsValue<Int> & ProvidesDialog {
        let count = try await TaskRepository.shared.countPending()
        let message = count == 0
            ? "Semua tugas sudah selesai! Luar biasa!"
            : "Kamu punya \(count) tugas yang belum selesai."
        return .result(value: count, dialog: "\(message)")
    }
}

// Intent untuk toggle dari widget
struct ToggleTaskCompletionIntent: AppIntent {
    static var title: LocalizedStringResource = "Toggle Status Tugas"
    
    @Parameter(title: "Task ID")
    var taskID: UUID
    
    func perform() async throws -> some IntentResult {
        try await TaskRepository.shared.toggle(id: taskID)
        return .result()
    }
}

// --- APP SHORTCUTS ---

struct TaskManagerShortcuts: AppShortcutsProvider {
    static var shortcutTileColor: ShortcutTileColor = .blue
    
    static var appShortcuts: [AppShortcut] {
        AppShortcut(
            intent: AddTaskIntent(),
            phrases: [
                "Tambah tugas di \(.applicationName)",
                "Buat tugas baru di \(.applicationName)",
                "Ingatkan saya di \(.applicationName)"
            ],
            shortTitle: "Tambah Tugas",
            systemImageName: "plus.circle.fill"
        )
        
        AppShortcut(
            intent: CompleteTaskIntent(),
            phrases: [
                "Selesaikan \(\.$task) di \(.applicationName)",
                "Tandai \(\.$task) selesai di \(.applicationName)"
            ],
            shortTitle: "Selesaikan Tugas",
            systemImageName: "checkmark.circle.fill"
        )
        
        AppShortcut(
            intent: GetPendingCountIntent(),
            phrases: [
                "Berapa tugas yang belum selesai di \(.applicationName)",
                "Tugas pending di \(.applicationName)"
            ],
            shortTitle: "Cek Tugas Pending",
            systemImageName: "list.number"
        )
    }
}

// --- WIDGET ---

struct TaskWidget: Widget {
    var body: some WidgetConfiguration {
        AppIntentConfiguration(
            kind: "TaskWidget",
            intent: TaskWidgetConfigIntent.self,
            provider: TaskWidgetProvider()
        ) { entry in
            TaskWidgetView(entry: entry)
        }
        .configurationDisplayName("Tugas Saya")
        .description("Lihat dan selesaikan tugas dari home screen.")
        .supportedFamilies([.systemSmall, .systemMedium])
    }
}

struct TaskWidgetView: View {
    let entry: TaskWidgetEntry
    
    var body: some View {
        VStack(alignment: .leading, spacing: 6) {
            Text("Tugas")
                .font(.caption.bold())
                .foregroundColor(.secondary)
            
            ForEach(entry.tasks.prefix(3)) { task in
                HStack(spacing: 8) {
                    Button(intent: ToggleTaskCompletionIntent(taskID: task.id)) {
                        Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
                            .foregroundColor(task.isCompleted ? .green : .gray)
                            .font(.body)
                    }
                    .buttonStyle(.plain)
                    
                    Text(task.title)
                        .font(.subheadline)
                        .strikethrough(task.isCompleted)
                        .foregroundColor(task.isCompleted ? .secondary : .primary)
                        .lineLimit(1)
                }
            }
            
            if entry.tasks.isEmpty {
                Text("Semua selesai!")
                    .font(.subheadline)
                    .foregroundColor(.secondary)
            }
        }
        .padding()
        .containerBackground(.fill.tertiary, for: .widget)
    }
}

private func formatted(_ date: Date) -> String {
    let f = DateFormatter()
    f.dateStyle = .short
    return f.string(from: date)
}

Use Case 2: Media Player dengan Control Widget

Kontrol playback via Control Center tanpa membuka app.

swift
import AppIntents
import WidgetKit

// --- Intents ---

struct PlayPauseIntent: AppIntent {
    static var title: LocalizedStringResource = "Play/Pause"
    
    func perform() async throws -> some IntentResult {
        await AudioPlayer.shared.togglePlayPause()
        return .result()
    }
}

struct SkipTrackIntent: AppIntent {
    static var title: LocalizedStringResource = "Skip Lagu"
    
    @Parameter(title: "Arah", default: .forward)
    var direction: SkipDirection
    
    func perform() async throws -> some IntentResult {
        switch direction {
        case .forward:  await AudioPlayer.shared.skipForward()
        case .backward: await AudioPlayer.shared.skipBackward()
        }
        return .result()
    }
}

enum SkipDirection: String, AppEnum {
    case forward, backward
    static var typeDisplayRepresentation: TypeDisplayRepresentation = "Arah Skip"
    static var caseDisplayRepresentations: [SkipDirection: DisplayRepresentation] = [
        .forward:  .init(title: "Maju", image: .init(systemName: "forward.fill")),
        .backward: .init(title: "Mundur", image: .init(systemName: "backward.fill"))
    ]
}

struct SetVolumeIntent: SetValueIntent {
    static var title: LocalizedStringResource = "Set Volume"
    
    @Parameter(title: "Volume", inclusiveRange: (0.0, 1.0))
    var value: Double
    
    func perform() async throws -> some IntentResult {
        await AudioPlayer.shared.setVolume(Float(value))
        return .result()
    }
}

// --- Control Widget ---

struct PlayPauseControl: ControlWidget {
    var body: some ControlWidgetConfiguration {
        StaticControlWidget(kind: "PlayPauseControl") {
            PlayPauseControlView()
        }
        .displayName("Play/Pause")
        .description("Kontrol playback langsung dari Control Center.")
    }
}

struct PlayPauseControlView: View {
    @Environment(\.controlWidgetValue) var isPlaying: Bool
    
    var body: some View {
        ControlWidgetToggle(
            "Musik",
            isOn: isPlaying,
            action: PlayPauseIntent()
        ) { isPlaying in
            Label(
                isPlaying ? "Pause" : "Play",
                systemImage: isPlaying ? "pause.fill" : "play.fill"
            )
        }
    }
}

// --- App Shortcuts ---

struct MusicShortcuts: AppShortcutsProvider {
    static var shortcutTileColor: ShortcutTileColor = .purple
    
    static var appShortcuts: [AppShortcut] {
        AppShortcut(
            intent: PlayPauseIntent(),
            phrases: [
                "Play di \(.applicationName)",
                "Pause \(.applicationName)",
                "Toggle musik di \(.applicationName)"
            ],
            shortTitle: "Play/Pause",
            systemImageName: "play.pause.fill"
        )
        
        AppShortcut(
            intent: SkipTrackIntent(),
            phrases: [
                "Skip lagu di \(.applicationName)",
                "Lagu berikutnya di \(.applicationName)"
            ],
            shortTitle: "Skip Lagu",
            systemImageName: "forward.fill"
        )
    }
}

Use Case 3: E-Commerce Quick Reorder

Pesan ulang produk favorit via Siri tanpa membuka app.

swift
import AppIntents

struct ProductEntity: AppEntity {
    var id: String
    var name: String
    var price: Double
    var imageURL: URL?
    var lastOrderedDate: Date?
    
    static var typeDisplayRepresentation: TypeDisplayRepresentation = TypeDisplayRepresentation(
        name: "Produk",
        numericFormat: "\(placeholder: .count) produk"
    )
    
    var displayRepresentation: DisplayRepresentation {
        .init(
            title: "\(name)",
            subtitle: "Rp\(Int(price).formatted())",
            image: imageURL.map { .init(url: $0) }
        )
    }
    
    static var defaultQuery = ProductEntityQuery()
}

struct ProductEntityQuery: EntityQuery, EntityStringQuery {
    func entities(for identifiers: [String]) async throws -> [ProductEntity] {
        try await ProductService.shared.fetch(ids: identifiers).map { ProductEntity(product: $0) }
    }
    
    func suggestedEntities() async throws -> [ProductEntity] {
        // Tampilkan produk yang sering dipesan — relevan untuk reorder
        try await ProductService.shared.fetchFrequentlyOrdered(limit: 10)
            .map { ProductEntity(product: $0) }
    }
    
    func entities(matching string: String) async throws -> [ProductEntity] {
        try await ProductService.shared.search(query: string).map { ProductEntity(product: $0) }
    }
}

struct ReorderProductIntent: AppIntent {
    static var title: LocalizedStringResource = "Pesan Ulang Produk"
    static var description = IntentDescription(
        "Pesan ulang produk yang pernah dibeli",
        categoryName: "Belanja"
    )
    
    @Parameter(title: "Produk", requestValueDialog: "Produk mana yang ingin dipesan ulang?")
    var product: ProductEntity
    
    @Parameter(title: "Jumlah", default: 1, inclusiveRange: (1, 10))
    var quantity: Int
    
    func perform() async throws -> some IntentResult & ProvidesDialog {
        // Cek stok dulu
        let stock = try await ProductService.shared.checkStock(id: product.id)
        guard stock >= quantity else {
            throw IntentError(description: "Stok tidak cukup. Tersedia: \(stock) item.")
        }
        
        // Konfirmasi sebelum pesan
        try await requestConfirmation(
            result: .result(dialog: "Pesan \(quantity)x \(product.name) seharga Rp\(Int(product.price * Double(quantity)).formatted())?"),
            confirmationActionName: .custom(
                acceptLabel: "Pesan",
                acceptImage: .init(systemName: "cart.fill.badge.plus"),
                denyLabel: "Batal"
            )
        )
        
        let order = try await OrderService.shared.placeOrder(
            productID: product.id,
            quantity: quantity
        )
        
        return .result(
            dialog: "\(quantity)x \(product.name) berhasil dipesan! Nomor pesanan: \(order.orderNumber)"
        )
    }
}

struct ECommerceShortcuts: AppShortcutsProvider {
    static var shortcutTileColor: ShortcutTileColor = .orange
    
    static var appShortcuts: [AppShortcut] {
        AppShortcut(
            intent: ReorderProductIntent(),
            phrases: [
                "Pesan ulang \(\.$product) di \(.applicationName)",
                "Beli lagi \(\.$product) dari \(.applicationName)",
                "Reorder \(\.$product) di \(.applicationName)"
            ],
            shortTitle: "Pesan Ulang",
            systemImageName: "arrow.clockwise.circle.fill"
        )
    }
}

// Custom error untuk intent
struct IntentError: LocalizedError {
    let description: String
    var errorDescription: String? { description }
}

extension ProductEntity {
    init(product: Product) {
        self.id = product.id
        self.name = product.name
        self.price = product.price
        self.imageURL = product.imageURL
        self.lastOrderedDate = product.lastOrderedDate
    }
}