Section 13/141 menit

13. Real Use Cases

13. Real Use Cases

Use Case 1: Habit Tracker dengan Interactive Widget

Aplikasi habit tracker yang memungkinkan pengguna menandai habit selesai langsung dari widget tanpa membuka app.

swift
import WidgetKit
import AppIntents
import SwiftUI

// Entry model lengkap
struct HabitEntry: TimelineEntry {
    let date: Date
    let habitID: String
    let habitName: String
    let emoji: String
    let isCompleted: Bool
    let streak: Int
    let totalToday: Int      // total habits hari ini
    let completedToday: Int  // yang sudah selesai
}

// Intent untuk menandai selesai
struct ToggleHabitIntent: AppIntent {
    static var title: LocalizedStringResource = "Toggle Habit"

    @Parameter(title: "Habit ID")
    var habitID: String

    @Parameter(title: "Habit Name")
    var habitName: String

    func perform() async throws -> some IntentResult {
        let key = "habitCompleted_\(habitID)_\(dateKey())"
        let defaults = UserDefaults(suiteName: "group.com.myapp.habits")
        let current = defaults?.bool(forKey: key) ?? false
        defaults?.set(!current, forKey: key)

        // Trigger widget reload
        WidgetCenter.shared.reloadTimelines(ofKind: "HabitWidget")
        return .result()
    }

    private func dateKey() -> String {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd"
        return formatter.string(from: Date())
    }
}

// Widget view yang adaptif
struct HabitWidgetView: View {
    @Environment(\.widgetFamily) var family
    let entry: HabitEntry

    var body: some View {
        switch family {
        case .systemSmall:
            SmallView(entry: entry)
        case .systemMedium:
            MediumView(entry: entry)
        default:
            SmallView(entry: entry)
        }
    }
}

struct SmallView: View {
    let entry: HabitEntry

    var body: some View {
        VStack(spacing: 8) {
            Text(entry.emoji)
                .font(.largeTitle)

            Button(intent: makeIntent()) {
                Text(entry.isCompleted ? "✓ Selesai" : "Tandai")
                    .font(.caption.bold())
                    .padding(.horizontal, 12)
                    .padding(.vertical, 6)
                    .background(entry.isCompleted ? Color.green : Color.blue)
                    .foregroundStyle(.white)
                    .clipShape(Capsule())
            }
            .buttonStyle(.plain)

            Text("🔥 \(entry.streak)")
                .font(.caption2)
        }
        .containerBackground(.fill.tertiary, for: .widget)
    }

    private func makeIntent() -> ToggleHabitIntent {
        let intent = ToggleHabitIntent()
        intent.habitID = entry.habitID
        intent.habitName = entry.habitName
        return intent
    }
}

struct MediumView: View {
    let entry: HabitEntry

    var body: some View {
        HStack(spacing: 16) {
            VStack(alignment: .leading, spacing: 4) {
                HStack {
                    Text(entry.emoji).font(.title2)
                    Text(entry.habitName).font(.headline)
                }
                Text("Streak: \(entry.streak) hari 🔥")
                    .font(.caption)
                    .foregroundStyle(.secondary)
                Text("\(entry.completedToday)/\(entry.totalToday) habits hari ini")
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }

            Spacer()

            Button(intent: makeIntent()) {
                Image(systemName: entry.isCompleted ? "checkmark.circle.fill" : "circle")
                    .font(.system(size: 44))
                    .foregroundStyle(entry.isCompleted ? .green : .blue)
            }
            .buttonStyle(.plain)
        }
        .padding()
        .containerBackground(.fill.tertiary, for: .widget)
    }

    private func makeIntent() -> ToggleHabitIntent {
        let intent = ToggleHabitIntent()
        intent.habitID = entry.habitID
        intent.habitName = entry.habitName
        return intent
    }
}

Use Case 2: Countdown Widget untuk Event Penting

Widget yang menampilkan countdown ke tanggal penting dengan update otomatis.

swift
import WidgetKit
import SwiftUI

struct EventEntry: TimelineEntry {
    let date: Date
    let eventName: String
    let eventDate: Date
    let daysRemaining: Int
    let hoursRemaining: Int
}

struct CountdownProvider: AppIntentTimelineProvider {
    typealias Intent = SelectEventIntent

    func placeholder(in context: Context) -> EventEntry {
        EventEntry(date: Date(), eventName: "Ulang Tahun", eventDate: Date().addingTimeInterval(86400 * 7), daysRemaining: 7, hoursRemaining: 0)
    }

    func snapshot(for configuration: SelectEventIntent, in context: Context) async -> EventEntry {
        makeEntry(for: configuration)
    }

    func timeline(for configuration: SelectEventIntent, in context: Context) async -> Timeline<EventEntry> {
        var entries: [EventEntry] = []
        let entry = makeEntry(for: configuration)
        entries.append(entry)

        // Update setiap jam
        let nextUpdate = Calendar.current.date(byAdding: .hour, value: 1, to: Date()) ?? Date()
        return Timeline(entries: entries, policy: .after(nextUpdate))
    }

    private func makeEntry(for config: SelectEventIntent) -> EventEntry {
        let targetDate = config.event?.date ?? Date().addingTimeInterval(86400 * 30)
        let name = config.event?.name ?? "Event"
        let now = Date()
        let diff = Calendar.current.dateComponents([.day, .hour], from: now, to: targetDate)

        return EventEntry(
            date: now,
            eventName: name,
            eventDate: targetDate,
            daysRemaining: max(0, diff.day ?? 0),
            hoursRemaining: max(0, diff.hour ?? 0)
        )
    }
}

struct CountdownWidgetView: View {
    @Environment(\.widgetFamily) var family
    let entry: EventEntry

    var body: some View {
        VStack(spacing: 6) {
            Text(entry.eventName)
                .font(family == .systemSmall ? .caption : .headline)
                .lineLimit(1)

            if entry.daysRemaining > 0 {
                HStack(alignment: .firstTextBaseline, spacing: 2) {
                    Text("\(entry.daysRemaining)")
                        .font(.system(size: family == .systemSmall ? 44 : 60, weight: .black, design: .rounded))
                        .foregroundStyle(.orange)
                    Text("hari")
                        .font(.title3)
                        .foregroundStyle(.secondary)
                }
            } else {
                Text("Hari ini! 🎉")
                    .font(.title2.bold())
                    .foregroundStyle(.green)
            }

            if family != .systemSmall {
                Text(entry.eventDate.formatted(date: .abbreviated, time: .omitted))
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }
        }
        .containerBackground(.fill.tertiary, for: .widget)
        .widgetURL(URL(string: "myapp://event/\(entry.eventName)"))
    }
}

Use Case 3: Expense Tracker Live Activity

Live Activity yang menampilkan status budget real-time saat pengguna sedang belanja.

swift
import ActivityKit
import SwiftUI

struct ShoppingActivityAttributes: ActivityAttributes {
    struct ContentState: Codable, Hashable {
        var spentAmount: Double
        var remainingBudget: Double
        var lastItemName: String
        var transactionCount: Int
    }

    var budgetName: String
    var totalBudget: Double
    var currency: String
}

struct ShoppingLiveActivityView: View {
    let context: ActivityViewContext<ShoppingActivityAttributes>

    private var progressFraction: Double {
        let spent = context.state.spentAmount
        let total = context.attributes.totalBudget
        return min(spent / total, 1.0)
    }

    private var statusColor: Color {
        switch progressFraction {
        case 0..<0.7: return .green
        case 0.7..<0.9: return .orange
        default: return .red
        }
    }

    var body: some View {
        VStack(spacing: 8) {
            HStack {
                Text(context.attributes.budgetName)
                    .font(.caption.bold())
                    .foregroundStyle(.secondary)
                Spacer()
                Text("\(context.state.transactionCount) transaksi")
                    .font(.caption2)
                    .foregroundStyle(.secondary)
            }

            HStack {
                Text(context.state.spentAmount, format: .currency(code: context.attributes.currency))
                    .font(.title2.bold())
                    .foregroundStyle(statusColor)

                Text("/")
                    .foregroundStyle(.secondary)

                Text(context.attributes.totalBudget, format: .currency(code: context.attributes.currency))
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
            }

            ProgressView(value: progressFraction)
                .tint(statusColor)

            Text("Terakhir: \(context.state.lastItemName)")
                .font(.caption2)
                .foregroundStyle(.secondary)
                .lineLimit(1)
        }
        .padding()
        .activityBackgroundTint(.black.opacity(0.85))
        .activitySystemActionForegroundColor(.white)
    }
}

// Manage dari app
class BudgetTracker {
    private var activity: Activity<ShoppingActivityAttributes>?

    func startShoppingSession(budget: Budget) throws {
        let attrs = ShoppingActivityAttributes(
            budgetName: budget.name,
            totalBudget: budget.amount,
            currency: budget.currency
        )
        let state = ShoppingActivityAttributes.ContentState(
            spentAmount: budget.spentAmount,
            remainingBudget: budget.amount - budget.spentAmount,
            lastItemName: "-",
            transactionCount: budget.transactionCount
        )
        activity = try Activity<ShoppingActivityAttributes>.request(
            attributes: attrs,
            contentState: state
        )
    }

    func recordPurchase(itemName: String, amount: Double, newTotal: Double, budget: Double) async {
        let newState = ShoppingActivityAttributes.ContentState(
            spentAmount: newTotal,
            remainingBudget: budget - newTotal,
            lastItemName: itemName,
            transactionCount: (activity?.contentState.transactionCount ?? 0) + 1
        )
        await activity?.update(using: newState)
    }

    func endSession() async {
        await activity?.end(using: activity?.contentState, dismissalPolicy: .immediate)
    }
}