๐Ÿƒ ๋ฐฐ๋‹ฌ ์ถ”์  Live Activity
๋งŒ๋“ค๊ธฐ

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 ๊ฐ€์ด๋“œ