🌐 EN

🏃 배달 추적 Live Activity
만들기

⭐ 난이도: ⭐⭐⭐ ⏱️ 예상 시간: 2-3h 📂 App Frameworks

ActivityKit을 활용해 배달 현황을 잠금 화면과 Dynamic Island에서 실시간으로 확인할 수 있는 Live Activity를 구현합니다.

📅 2025.02 ⏱ 총 실습 2시간 📱 iOS 16.1+ / Xcode 15+ 🎯 중급

📖 Live Activity란?

Live Activity는 진행 중인 작업의 실시간 상태를 잠금 화면과 Dynamic Island에 표시합니다. 위젯과 달리 시간 제한(최대 8시간)이 있고, 실시간 업데이트가 가능합니다.

🍎 Live Activity 적합 사례

✅ 좋은 사례
배달/택시 추적, 스포츠 경기, 타이머, 음악 재생, 항공편 정보

❌ 나쁜 사례
날씨(→ Widget), 캘린더 알림(→ Notification), 주가(→ Widget)

🏗️ ActivityAttributes 정의

DeliveryAttributes.swift Swift
import ActivityKit

struct DeliveryAttributes: ActivityAttributes {
    // Static: 변경 불가
    let orderNumber: String
    let storeName: String
    
    // ContentState: 실시간 업데이트
    struct ContentState: Codable, Hashable {
        let status: DeliveryStatus
        let estimatedArrival: Date
        let driverName: String?
    }
}

enum DeliveryStatus: String, Codable {
    case preparing  // 준비 중
    case pickedUp   // 픽업 완료
    case nearby     // 근처 도착
    case delivered  // 배달 완료
}

🏝️ Dynamic Island 레이아웃

Dynamic Island는 3가지 상태가 있습니다: Compact, Minimal, Expanded

DeliveryLiveActivity.swift Swift
struct DeliveryLiveActivity: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: DeliveryAttributes.self) { context in
            // 잠금 화면
            LockScreenView(context: context)
        } dynamicIsland: { context in
            DynamicIsland {
                // Expanded
                DynamicIslandExpandedRegion(.leading) {
                    Image(systemName: "person.circle")
                }
                DynamicIslandExpandedRegion(.trailing) {
                    Image(systemName: context.state.status.symbol)
                }
                DynamicIslandExpandedRegion(.center) {
                    Text(context.attributes.storeName)
                }
                DynamicIslandExpandedRegion(.bottom) {
                    ProgressView(value: context.state.progress)
                }
            } compactLeading: {
                Image(systemName: "bicycle")
            } compactTrailing: {
                Text(context.state.estimatedArrival, style: .timer)
            } minimal: {
                Image(systemName: "bicycle")
            }
        }
    }
}

📱 LockScreen View 구현

LockScreenView.swift Swift
struct LockScreenView: View {
    let context: ActivityViewContext<DeliveryAttributes>

    var body: some View {
        HStack(spacing: 12) {
            // 상태 아이콘
            Image(systemName: context.state.status.symbol)
                .font(.title2)
                .foregroundStyle(.blue)

            VStack(alignment: .leading, spacing: 4) {
                Text(context.attributes.storeName)
                    .font(.headline)

                Text(context.state.status.description)
                    .font(.caption)
                    .foregroundStyle(.secondary)

                // 도착 예정 시간
                Text(context.state.estimatedArrival, style: .relative)
                    .font(.caption2)
                    .foregroundStyle(.blue)
            }

            Spacer()

            // 진행률
            ProgressView(value: context.state.progress)
                .progressViewStyle(.circular)
        }
        .padding(16)
    }
}

⚡ Activity 생명주기

ActivityManager.swift Swift
class DeliveryActivityManager {
    private var currentActivity: Activity<DeliveryAttributes>?

    // 1. Activity 시작
    func startActivity(orderNumber: String, storeName: String) async throws {
        let attributes = DeliveryAttributes(
            orderNumber: orderNumber,
            storeName: storeName
        )

        let initialState = DeliveryAttributes.ContentState(
            status: .preparing,
            estimatedArrival: Date().addingTimeInterval(1800),
            driverName: nil
        )

        currentActivity = try Activity.request(
            attributes: attributes,
            content: ActivityContent(state: initialState, staleDate: nil),
            pushType: .token  // Push Notification 지원
        )
    }

    // 2. 상태 업데이트
    func updateStatus(_ status: DeliveryStatus, driverName: String? = nil) async {
        let newState = DeliveryAttributes.ContentState(
            status: status,
            estimatedArrival: Date().addingTimeInterval(600),
            driverName: driverName
        )

        await currentActivity?.update(
            ActivityContent(state: newState, staleDate: nil)
        )
    }

    // 3. Activity 종료
    func endActivity() async {
        let finalState = DeliveryAttributes.ContentState(
            status: .delivered,
            estimatedArrival: Date(),
            driverName: nil
        )

        await currentActivity?.end(
            ActivityContent(state: finalState, staleDate: nil),
            dismissalPolicy: .after(Date().addingTimeInterval(3600))  // 1시간 후 자동 제거
        )
    }
}

⚙️ Info.plist 설정

Live Activity를 사용하려면 Info.plist에 권한을 추가해야 합니다:

Info.plist XML
<key>NSSupportsLiveActivities</key>
<true/>

🔔 Push Notification 연동 (선택)

서버에서 원격으로 업데이트하려면 Push Token을 사용합니다:

PushToken.swift Swift
// Push Token 가져오기
for await pushToken in Activity<DeliveryAttributes>.pushToStartTokenUpdates {
    // 서버로 Push Token 전송
    await sendTokenToServer(pushToken)
}

⚠️ 주의사항

🚨 Live Activity 제한사항

⏱️ 시간 제한
최대 8시간 동안만 표시됩니다. 그 이후에는 자동으로 제거됩니다.

🔋 배터리 영향
너무 자주 업데이트하면 배터리 소모가 큽니다. 최소 5초 간격을 권장합니다.

📱 기기 제한
Dynamic Island는 iPhone 14 Pro 이상에서만 표시됩니다. 다른 기기에서는 잠금 화면에만 표시됩니다.

🎨 UI 제약
탭 제스처, 애니메이션, 비디오는 지원되지 않습니다. Button, Toggle, TextField도 사용할 수 없습니다.

🎉

챌린지 완료!

Dynamic Island와 잠금 화면에서 배달 현황을 실시간으로 확인할 수 있는 Live Activity를 완성했습니다!

📦 학습 자료

📚
DocC 튜토리얼
Xcode에서 바로 실습
💻
GitHub 프로젝트
전체 소스코드
🍎
Apple HIG 원문
Live Activities 가이드

📎 Apple 공식 자료

📘 공식 문서 💻 샘플 코드 🎬 WWDC 세션