Section 10/141 menit
10. Live Activities dan Dynamic Island
10. Live Activities dan Dynamic Island
Live Activities (iOS 16.1+) adalah widget yang bisa diupdate secara real-time dan ditampilkan di Lock Screen serta Dynamic Island.
Definisikan ActivityAttributes
swift
import ActivityKit
struct DeliveryAttributes: ActivityAttributes {
// ContentState: data yang BISA berubah selama aktivitas berlangsung
struct ContentState: Codable, Hashable {
var currentStep: DeliveryStep
var estimatedMinutes: Int
var courierName: String
}
// Properties di luar ContentState: data yang TIDAK berubah
var orderId: String
var restaurantName: String
var itemsSummary: String
}
enum DeliveryStep: String, Codable, Hashable {
case confirmed = "Pesanan Dikonfirmasi"
case preparing = "Sedang Dimasak"
case onTheWay = "Dalam Perjalanan"
case nearYou = "Hampir Tiba"
case delivered = "Terkirim"
var iconName: String {
switch self {
case .confirmed: return "checkmark.circle"
case .preparing: return "flame"
case .onTheWay: return "bicycle"
case .nearYou: return "location.fill"
case .delivered: return "house.fill"
}
}
}
Tampilan Live Activity
swift
struct DeliveryLiveActivityView: View {
let context: ActivityViewContext<DeliveryAttributes>
var body: some View {
HStack(spacing: 12) {
Image(systemName: context.state.currentStep.iconName)
.font(.title2)
.foregroundStyle(.orange)
VStack(alignment: .leading, spacing: 2) {
Text(context.state.currentStep.rawValue)
.font(.headline)
Text(context.attributes.restaurantName)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
VStack(alignment: .trailing) {
Text("\(context.state.estimatedMinutes) mnt")
.font(.headline.bold())
Text("Estimasi")
.font(.caption2)
.foregroundStyle(.secondary)
}
}
.padding()
.activityBackgroundTint(.black.opacity(0.8))
}
}
// Dynamic Island configuration
struct DeliveryWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: DeliveryAttributes.self) { context in
// Lock Screen / Notification-style
DeliveryLiveActivityView(context: context)
} dynamicIsland: { context in
DynamicIsland {
// Expanded — saat Dynamic Island di-expand
DynamicIslandExpandedRegion(.leading) {
Image(systemName: context.state.currentStep.iconName)
.font(.title)
.foregroundStyle(.orange)
}
DynamicIslandExpandedRegion(.trailing) {
Text("\(context.state.estimatedMinutes) mnt")
.font(.title2.bold())
}
DynamicIslandExpandedRegion(.bottom) {
Text(context.state.currentStep.rawValue)
.font(.headline)
}
} compactLeading: {
// Compact — sisi kiri Dynamic Island saat collapsed
Image(systemName: context.state.currentStep.iconName)
.foregroundStyle(.orange)
} compactTrailing: {
// Compact — sisi kanan
Text("\(context.state.estimatedMinutes)m")
.font(.caption.bold())
} minimal: {
// Paling kecil, saat ada banyak aktivitas aktif
Image(systemName: context.state.currentStep.iconName)
}
}
}
}
Mengelola Live Activity dari App
swift
import ActivityKit
class DeliveryTracker {
private var activity: Activity<DeliveryAttributes>?
func startTracking(order: Order) throws {
guard ActivityAuthorizationInfo().areActivitiesEnabled else {
throw DeliveryError.liveActivitiesNotEnabled
}
let attributes = DeliveryAttributes(
orderId: order.id,
restaurantName: order.restaurantName,
itemsSummary: order.itemsSummary
)
let initialState = DeliveryAttributes.ContentState(
currentStep: .confirmed,
estimatedMinutes: 35,
courierName: "Belum ditugaskan"
)
activity = try Activity<DeliveryAttributes>.request(
attributes: attributes,
contentState: initialState,
pushType: .token // Gunakan .token jika update via APNs push
)
}
func updateStatus(step: DeliveryStep, etaMinutes: Int, courierName: String) async {
let newState = DeliveryAttributes.ContentState(
currentStep: step,
estimatedMinutes: etaMinutes,
courierName: courierName
)
await activity?.update(using: newState)
}
func endActivity(delivered: Bool) async {
let finalState = DeliveryAttributes.ContentState(
currentStep: .delivered,
estimatedMinutes: 0,
courierName: activity?.contentState.courierName ?? ""
)
// Tampilkan state akhir, lalu hapus setelah beberapa detik
await activity?.end(using: finalState, dismissalPolicy: .after(.now + 5))
}
}