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