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