Live Activities và Dynamic Island trong iOS 26: Hướng Dẫn ActivityKit Toàn Diện với SwiftUI

Hướng dẫn xây Live Activities và Dynamic Island trên iOS 26 với ActivityKit và SwiftUI: từ ActivityAttributes, thiết kế bốn state Dynamic Island, cập nhật local và APNs push, tới broadcast channels và interactive App Intents. Kèm code chạy được và bài học thực tế.

Live Activities iOS 26: ActivityKit Guide

Cập nhật: 16 tháng 8, 2026

Live Activities trong iOS 26 là các view SwiftUI ngắn hạn được quản lý bởi framework ActivityKit, xuất hiện trên Lock Screen và Dynamic Island để hiển thị thông tin cập nhật theo thời gian thực về một sự kiện đang diễn ra (trận đấu, chuyến bay, giao hàng hay bài tập luyện). Trong iOS 26, Apple mở rộng đáng kể khả năng của Live Activities: lịch trình khởi chạy tự động cho ứng dụng bên thứ ba, hiển thị paired-device trên watchOS 11 và macOS 26, phong cách Dynamic Island landscape mới, và Swift 6 strict concurrency mặc định trong Widget Extension. Bài viết này dựng một Live Activity hoàn chỉnh từ đầu, kèm code chạy được và các lưu ý thực chiến sau khi tôi ship trên nhiều app khách hàng.

  • Live Activity là một widget ngắn hạn dùng ActivityKit + WidgetKit, render trên Lock Screen và Dynamic Island; không cần user opt-in để bắt đầu.
  • iOS 26 bổ sung push-to-start theo lịch cho app bên thứ ba, paired-device display trên watchOS 11 và macOS 26, cùng phong cách Dynamic Island landscape khi xoay ngang iPhone.
  • Dynamic Island có bốn state (compact leading, compact trailing, minimal, expanded). Bạn khai báo tất cả trong DynamicIsland block và hệ thống chọn state phù hợp.
  • Cập nhật cục bộ dùng activity.update(_:). Cập nhật từ server dùng APNs push với apns-push-type: liveactivity, hoặc channel cho broadcast tới hàng triệu thiết bị.
  • Payload update tối đa 4KB; iOS 26 fail silent với telemetry entry thay vì crash extension nếu vượt budget.
  • Interactive Live Activity yêu cầu App Intents (Button(intent:)). Live Activity chỉ nhận tương tác qua intent, không có onTapGesture tự do.

Live Activity là gì và khi nào nên dùng?

Live Activity là một widget đặc biệt được ràng buộc với một sự kiện thời gian thực. Không giống widget thông thường chạy vô thời hạn trên Home Screen, một Live Activity chỉ tồn tại khi sự kiện đang diễn ra và tự động biến mất khi bạn gọi end(_:dismissalPolicy:). Không giống push notification stack thành danh sách, Live Activity cập nhật tại chỗ trên cùng một card. Và không giống một app đang chạy, nó tiêu thụ tài nguyên rất khiêm tốn vì được render trong Widget Extension và bị đóng băng khi không cần update.

Trong nhiều dự án khách hàng, tôi dùng Live Activity cho ba loại use case rõ ràng: tracking (giao hàng, xe công nghệ, chuyến bay), score/status (trận đấu thể thao, kết quả bầu cử, vote real-time), và timer/progress (Pomodoro, luyện tập, timer bếp). Nếu event của bạn không có "trạng thái đang chạy" rõ ràng và không đáng để chiếm một dòng trên Lock Screen, thì đừng ép nó thành Live Activity. User sẽ tắt Live Activity của app bạn trong Settings và bạn mất luôn kênh này.

Một điểm dễ bị bỏ qua: Live Activity không yêu cầu user cấp quyền notification. Người dùng chỉ cần bật toggle "Live Activities" trong Settings của app bạn (mặc định là ON), và app có thể khởi chạy một activity từ foreground. Honestly, đây là kênh giao tiếp mạnh hơn cả push notification cho các sự kiện critical như "đơn hàng đang giao" hay "cuộc gọi đến qua VoIP".

Có gì mới trong iOS 26 cho Live Activities?

iOS 26 là bản cập nhật lớn nhất cho ActivityKit kể từ khi framework này ra mắt trong iOS 16.1. Nếu bạn đã ship Live Activity trên iOS 17 hoặc 18, bốn thay đổi dưới đây đáng để refactor:

  • Scheduled Live Activities cho app bên thứ ba. Trước đây chỉ app Sports của Apple được phép lên lịch Live Activity tự động khởi chạy khi trận đấu bắt đầu. Từ iOS 26, mọi app đều có thể làm được qua API mới với thời gian bắt đầu và metadata sự kiện. Cực kỳ hữu ích cho app booking, calendar, sports và event ticketing.
  • Paired-device display trên watchOS 11 và macOS 26. Live Activity giờ tự động mirror sang đồng hồ Apple Watch (Smart Stack) và menu bar macOS mà không cần bạn viết thêm code Watch app riêng. Đây là thay đổi tôi thấy tác động lớn nhất tới UX. User không cần mở iPhone để xem tiến độ.
  • Landscape Dynamic Island style. Khi user xoay iPhone ngang (ví dụ xem video), Dynamic Island có phong cách mới rộng hơn, cho phép bạn hiển thị nhiều thông tin hơn thay vì chỉ compact pill.
  • Silent payload failure. Trong iOS 25 trở về trước, payload vượt quá budget (4KB) khiến widget extension crash. iOS 26 fail silent với một telemetry entry trong Console.app, an toàn hơn nhưng bạn cần chủ động monitor bằng Instruments.

Widget Extension template trong Xcode 26 mặc định bật Swift 6 strict concurrency. Nếu bạn migrate từ code cũ, chuẩn bị tinh thần đối mặt với các warning về @MainActor isolation cho các view. Bài Swift Concurrency 2026 với async/await và Actors đã cover chi tiết cách sửa các lỗi này, nên tôi không lặp lại ở đây.

Thiết lập dự án: Xcode 26, Info.plist và Widget Extension

Trước khi viết một dòng ActivityKit nào, bạn cần dựng cấu trúc project chuẩn. Yêu cầu tối thiểu: Xcode 26 với iOS 26 SDK, và một iPhone 14 Pro trở lên (bao gồm mọi iPhone 15/16/17 Pro và các iPhone 26) để test Dynamic Island. Simulator hỗ trợ tốt cho Lock Screen nhưng Dynamic Island trên simulator không giống 100% device thật, đặc biệt là animation transition giữa compact và expanded.

Bước 1. Bật cờ hỗ trợ Live Activity trong Info.plist của app target chính:

<key>NSSupportsLiveActivities</key>
<true/>

<!-- Chỉ thêm nếu bạn thật sự cần cập nhật với tần suất cao (dưới 1 giây) -->
<key>NSSupportsLiveActivitiesFrequentUpdates</key>
<true/>

Cảnh báo: NSSupportsLiveActivitiesFrequentUpdates tiêu tốn budget năng lượng của app bạn. Với một stopwatch hay countdown timer thì cần, nhưng với score board cập nhật mỗi 30 giây thì không, hệ thống sẽ throttle bạn nếu lạm dụng.

Bước 2. Thêm Widget Extension target: File → New → Target → Widget Extension. Đặt tên (ví dụ DeliveryLiveActivity) và tick "Include Live Activity". Xcode 26 tạo sẵn một file template với ActivityAttributesWidget struct.

Bước 3. Chia sẻ code giữa app và extension. Tạo một Swift package hoặc framework chứa struct ActivityAttributes để cả app (nơi start activity) và widget extension (nơi render) đều import được. Đừng để một file trong hai target, bug ngầm khó debug lắm.

Xây dựng Live Activity đầu tiên với ActivityAttributes

Trái tim của Live Activity là struct ActivityAttributes. Nó định nghĩa hai loại dữ liệu: các thuộc tính bất biến của sự kiện (ví dụ tên nhà hàng cho đơn giao đồ ăn) và một struct ContentState chứa dữ liệu thay đổi theo thời gian (trạng thái đơn, ETA còn lại).

import ActivityKit
import Foundation

// File này nên nằm trong một shared framework/package
public struct DeliveryAttributes: ActivityAttributes {
    // ContentState là dữ liệu thay đổi mỗi lần update
    public struct ContentState: Codable, Hashable {
        public var status: DeliveryStatus
        public var etaMinutes: Int
        public var driverName: String?
        public var driverLocation: String?

        public init(status: DeliveryStatus, etaMinutes: Int,
                    driverName: String? = nil, driverLocation: String? = nil) {
            self.status = status
            self.etaMinutes = etaMinutes
            self.driverName = driverName
            self.driverLocation = driverLocation
        }
    }

    // Thuộc tính cố định trong suốt vòng đời của Live Activity
    public let orderNumber: String
    public let restaurantName: String
    public let totalItems: Int

    public init(orderNumber: String, restaurantName: String, totalItems: Int) {
        self.orderNumber = orderNumber
        self.restaurantName = restaurantName
        self.totalItems = totalItems
    }
}

public enum DeliveryStatus: String, Codable, Hashable {
    case preparing, cooking, ready, pickedUp, delivering, delivered

    public var displayText: String {
        switch self {
        case .preparing:  return "Đang chuẩn bị"
        case .cooking:    return "Đang nấu"
        case .ready:      return "Sẵn sàng lấy"
        case .pickedUp:   return "Tài xế đã lấy"
        case .delivering: return "Đang giao"
        case .delivered:  return "Đã giao"
        }
    }
}

Trong app chính, khởi chạy activity từ một action của user (ví dụ sau khi thanh toán thành công):

import ActivityKit

@MainActor
func startDeliveryActivity(order: Order) async throws -> Activity<DeliveryAttributes> {
    // Kiểm tra user có bật Live Activities không
    guard ActivityAuthorizationInfo().areActivitiesEnabled else {
        throw ActivityError.disabledByUser
    }

    let attributes = DeliveryAttributes(
        orderNumber: order.number,
        restaurantName: order.restaurantName,
        totalItems: order.items.count
    )

    let initialState = DeliveryAttributes.ContentState(
        status: .preparing,
        etaMinutes: 35
    )

    let content = ActivityContent(
        state: initialState,
        staleDate: Date().addingTimeInterval(60 * 60) // 1 giờ
    )

    return try Activity.request(
        attributes: attributes,
        content: content,
        pushType: .token  // yêu cầu APNs token để cập nhật từ server
    )
}

Chú ý tham số staleDate. Nếu không có update mới trước thời điểm này, hệ thống sẽ mờ Live Activity đi (không tự dismiss) và bạn có cơ hội gửi push tiếp. Với đơn hàng thức ăn, tôi thường đặt stale date là 2× ETA để tránh false alarm khi mạng kém.

Thiết kế Dynamic Island trong SwiftUI

Đây là phần thú vị và cũng dễ sai nhất. Bạn khai báo Live Activity bằng một ActivityConfiguration trong Widget bundle, cung cấp cả layout Lock Screen lẫn ba trạng thái Dynamic Island: compact (leading/trailing), minimal, và expanded. Hệ thống tự chọn state nào để hiện dựa vào số lượng activity đang chạy và context.

import ActivityKit
import WidgetKit
import SwiftUI

struct DeliveryLiveActivity: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: DeliveryAttributes.self) { context in
            // Lock Screen / banner presentation
            LockScreenView(context: context)
                .activityBackgroundTint(Color.orange.opacity(0.2))
                .activitySystemActionForegroundColor(Color.orange)
        } dynamicIsland: { context in
            DynamicIsland {
                // Expanded: khi user long-press hoặc chỉ có 1 activity
                DynamicIslandExpandedRegion(.leading) {
                    Label {
                        Text(context.attributes.restaurantName)
                            .font(.caption)
                    } icon: {
                        Image(systemName: "fork.knife")
                            .foregroundStyle(.orange)
                    }
                }
                DynamicIslandExpandedRegion(.trailing) {
                    Text("\(context.state.etaMinutes) phút")
                        .font(.caption)
                        .monospacedDigit()
                }
                DynamicIslandExpandedRegion(.bottom) {
                    VStack(alignment: .leading, spacing: 4) {
                        Text(context.state.status.displayText)
                            .font(.headline)
                        ProgressView(value: progress(for: context.state.status))
                            .tint(.orange)
                    }
                }
            } compactLeading: {
                Image(systemName: "bag.fill")
                    .foregroundStyle(.orange)
            } compactTrailing: {
                Text("\(context.state.etaMinutes)m")
                    .monospacedDigit()
            } minimal: {
                Image(systemName: "bag.fill")
                    .foregroundStyle(.orange)
            }
            .widgetURL(URL(string: "myapp://order/\(context.attributes.orderNumber)"))
            .keylineTint(.orange)
        }
    }

    private func progress(for status: DeliveryStatus) -> Double {
        switch status {
        case .preparing:  return 0.15
        case .cooking:    return 0.35
        case .ready:      return 0.5
        case .pickedUp:   return 0.65
        case .delivering: return 0.85
        case .delivered:  return 1.0
        }
    }
}

Bốn state của Dynamic Island có luật riêng. Compact leading/trailing chỉ có ~44pt chiều rộng nên chỉ nhét được icon nhỏ hoặc 3–4 ký tự. Minimal xuất hiện khi có nhiều activity cùng lúc và bị co lại thành pill nhỏ. Expanded có full width, dùng cho long-press hoặc khi chỉ có một activity active.

Trong iOS 26, region bottom hỗ trợ thêm interactive controls như ButtonToggle. Đừng nhồi quá nhiều, Apple's Human Interface Guidelines khuyên tối đa 2 button trong expanded region. Xem thêm bài Liquid Glass trong SwiftUI để phối màu Live Activity đồng bộ với design language iOS 26.

Cập nhật Live Activity: local update và APNs push

Có hai cách cập nhật Live Activity: từ chính app khi đang foreground/background ngắn, hoặc từ server qua Apple Push Notification service. Trong thực tế bạn dùng cả hai: local update cho phản hồi tức thì khi user tương tác, push update cho các thay đổi từ backend (tài xế cập nhật vị trí, trạng thái đơn thay đổi).

Local update khá đơn giản:

@MainActor
func updateDeliveryActivity(_ activity: Activity<DeliveryAttributes>,
                            status: DeliveryStatus,
                            etaMinutes: Int) async {
    let newState = DeliveryAttributes.ContentState(
        status: status,
        etaMinutes: etaMinutes
    )
    let content = ActivityContent(
        state: newState,
        staleDate: Date().addingTimeInterval(60 * 30)
    )
    await activity.update(content)
}

@MainActor
func endDeliveryActivity(_ activity: Activity<DeliveryAttributes>) async {
    let finalState = DeliveryAttributes.ContentState(
        status: .delivered,
        etaMinutes: 0
    )
    await activity.end(
        ActivityContent(state: finalState, staleDate: nil),
        dismissalPolicy: .after(Date().addingTimeInterval(60 * 10))
    )
}

dismissalPolicy có ba giá trị: .default (giữ trên Lock Screen tối đa 4 giờ), .immediate (biến mất ngay), và .after(Date) (biến mất tại thời điểm chỉ định, tối đa 4 giờ). Với đơn giao hàng, tôi giữ 10 phút sau khi giao để user còn kịp rate driver mà không cần mở app.

Push update phức tạp hơn vì cần lấy push token và gọi APNs. Sau khi Activity.request trả về, quan sát activity.pushTokenUpdates để gửi token về server:

func registerActivityForPushUpdates(_ activity: Activity<DeliveryAttributes>) {
    Task {
        for await tokenData in activity.pushTokenUpdates {
            let token = tokenData.map { String(format: "%02x", $0) }.joined()
            await MyBackend.uploadLiveActivityToken(
                token, orderNumber: activity.attributes.orderNumber
            )
        }
    }
}

Trên server, gửi HTTP/2 request tới APNs với header apns-push-type: liveactivity và payload dạng:

{
  "aps": {
    "timestamp": 1736930400,
    "event": "update",
    "content-state": {
      "status": "delivering",
      "etaMinutes": 8,
      "driverName": "Nam",
      "driverLocation": "Cách 1.2km"
    },
    "stale-date": 1736932200,
    "alert": {
      "title": "Đơn hàng đang giao",
      "body": "Tài xế cách bạn 1.2km"
    }
  }
}

Xem thêm tài liệu chính thức của Apple về cập nhật Live Activity qua APNs để biết chi tiết các trường event, relevance-scoredismissal-date.

Broadcast Channels: cập nhật đồng loạt hàng triệu thiết bị

Traditional APNs push gửi một message tới một token, hoàn hảo cho use case cá nhân như tracking đơn hàng. Nhưng với trận đấu bóng đá có 500 nghìn người xem, gửi 500 nghìn push request là bất khả thi. Từ iOS 18, Apple giới thiệu Broadcast Channels: một payload duy nhất được APNs fan-out tới mọi device subscribe channel đó.

Trong iOS 26, broadcast channel được cải thiện với storage policy mới cho phép giữ message gần nhất (Most Recent Message) để device mới join channel sẽ nhận ngay update mới nhất thay vì phải đợi push tiếp theo.

Bước 1. Tạo channel trên Push Notifications Console (hoặc dùng REST API). Bạn nhận được một Channel ID dạng UUID.

Bước 2. Subscribe channel khi request activity:

import ActivityKit

@MainActor
func startMatchActivity(match: Match, channelID: String) async throws {
    let attributes = MatchAttributes(
        matchID: match.id,
        homeTeam: match.homeTeam,
        awayTeam: match.awayTeam
    )
    let initialState = MatchAttributes.ContentState(
        homeScore: 0, awayScore: 0, minute: 0, phase: .firstHalf
    )
    let content = ActivityContent(
        state: initialState,
        staleDate: Date().addingTimeInterval(60 * 130) // 2h10m
    )
    _ = try Activity.request(
        attributes: attributes,
        content: content,
        pushType: .channel(channelID)
    )
}

Bước 3. Gửi broadcast update từ server tới endpoint /3/broadcasts/apns/{channel-id} với payload tương tự push thông thường nhưng không cần push token. Một request cập nhật score được delivery tới tất cả subscriber, gồm cả device offline (nếu bạn chọn storage policy Most Recent Message).

Với các app sports như VBA hay V-League app, dùng channel là cách duy nhất để scale. Tôi từng làm một prototype sports app dùng traditional per-token push và hóa đơn APNs tăng gấp 40× khi đạt 20 nghìn user đồng thời. Chuyển sang channel giảm cost 99%.

Interactive Live Activity với App Intents

Từ iOS 17, Live Activity trong expanded Dynamic Island region và Lock Screen banner có thể chứa Button hoặc Toggle. Nhưng tương tác không xảy ra qua closure trực tiếp, bạn phải kèm một App Intent để hệ thống chạy trong background hoặc chuyển user vào app.

import AppIntents

struct PauseWorkoutIntent: AppIntent {
    static var title: LocalizedStringResource = "Tạm dừng bài tập"
    static var description = IntentDescription("Tạm dừng workout đang chạy.")

    // Không mở app, chạy background ngắn
    static var openAppWhenRun: Bool = false

    @Parameter(title: "Workout ID")
    var workoutID: String

    init() {}

    init(workoutID: String) {
        self.workoutID = workoutID
    }

    func perform() async throws -> some IntentResult {
        try await WorkoutManager.shared.pauseWorkout(id: workoutID)
        return .result()
    }
}

Sử dụng intent trong Dynamic Island expanded region:

DynamicIslandExpandedRegion(.bottom) {
    HStack {
        Button(intent: PauseWorkoutIntent(workoutID: context.attributes.workoutID)) {
            Label("Tạm dừng", systemImage: "pause.fill")
        }
        .buttonStyle(.bordered)
        .tint(.orange)

        Button(intent: EndWorkoutIntent(workoutID: context.attributes.workoutID)) {
            Label("Kết thúc", systemImage: "stop.fill")
        }
        .buttonStyle(.borderedProminent)
        .tint(.red)
    }
}

Có ba giới hạn quan trọng: intent perform tối đa ~10 giây trước khi bị kill; UI update chỉ propagate khi bạn gọi activity.update(_:) trong intent; và Toggle cần một SetValueIntent@Parameter kiểu Bool. Bài App Intents Interactive Snippets trong iOS 26 đi sâu vào các pattern intent nâng cao mà bạn có thể tái sử dụng ở đây.

Live Activity trên iPad, Mac và Apple Watch

Đây là phần mà iOS 26 thay đổi bản đồ nhiều nhất. Trước đây Live Activity chỉ hiện trên iPhone. Từ 2026, hành vi khác nhau đáng kể trên từng platform và bạn cần biết để không viết code "iPhone-first" rồi phải fix sau:

  • iPadOS 26: Live Activity hiện trên Lock Screen tương tự iPhone, và trong Stage Manager có một overlay pill nhỏ ở góc. Không có Dynamic Island (iPad không có notch), nên bạn thiết kế Lock Screen layout thật kỹ vì đó là bề mặt chính.
  • Mac Catalyst và macOS 26 native: Với paired-device display mới, khi iPhone và Mac đăng nhập cùng Apple ID, một Live Activity active trên iPhone tự động hiện trong menu bar Mac (dạng pill nhỏ hiển thị compact state). User click vào sẽ mở expanded view. Bạn không cần viết thêm code, nhưng cần test kỹ compact state hiển thị dễ đọc trên menu bar background (dark mode và light mode).
  • watchOS 11+: Live Activity mirror vào Smart Stack với ưu tiên cao khi relevance-score lớn. Watch dùng cùng SwiftUI layout của compact leading + trailing, nhưng scale up. Test trên Ultra và Series 10 vì kích thước khác nhau.

Cross-platform breadth là nơi bạn tạo dấu ấn UX: một app delivery mà user vừa nhìn tay được ETA trên Apple Watch, vừa thấy driver location trên menu bar Mac, vừa mở expanded card trên iPhone. Cảm giác app "hiện diện" khắp hệ sinh thái. Nhưng nhớ rằng cùng một payload phải hoạt động trên ba màn hình khác nhau, nên tránh dùng font quá nhỏ hoặc màu tương phản kém.

Best practices và giới hạn cần biết

Sau khi ship Live Activity trên nhiều app khác nhau (từ đặt xe, giao hàng đến workout) dưới đây là những bài học tôi rút ra:

  • Payload budget 4KB. Cả ActivityAttributes lẫn ContentState combined phải < 4KB khi encode JSON. Ảnh nên là system image hoặc URL, tuyệt đối không nhồi base64.
  • Tối đa 8 activity cùng lúc mỗi app. Nếu app bạn cho phép nhiều order/trận đấu, hãy có logic dismiss activity cũ trước khi tạo mới thứ 9. Nếu không, Activity.request sẽ throw.
  • Update budget. Không có con số cứng nhưng Apple throttle app update quá thường xuyên. Nguyên tắc: chỉ update khi state thay đổi có ý nghĩa. Không update mỗi giây trừ khi có NSSupportsLiveActivitiesFrequentUpdates và use case chính đáng.
  • Vòng đời tối đa 8 giờ (12 giờ với Sports mới trong iOS 26). Sau đó activity bị system end tự động. Với event dài (chuyến bay 15h), cân nhắc restart activity mới.
  • Test kỹ background transition. Update trong app foreground khác trong background. Dùng Instruments > Widget template để theo dõi throttling.
  • Không call Activity.request từ background. Chỉ start được từ foreground hoặc từ một Push-to-Start payload đặc biệt (iOS 17.2+). Dev hay quên rồi thắc mắc tại sao push không tạo activity mới.
  • Localize đầy đủ. Vì Live Activity render bởi widget extension, mọi string phải nằm trong Localizable.strings của extension target, không phải app target.

Cuối cùng, đừng bỏ qua Human Interface Guidelines cho Live Activities. Apple review app rất khắt khe với Live Activity: activity dùng cho quảng cáo, engagement bait, hay không có timing rõ ràng đều bị reject. Xem tài liệu ActivityKit chính thức để biết API mới nhất trước khi ship production.

Câu hỏi thường gặp

Live Activity có hoạt động trên iPad không?

Có. Từ iPadOS 26, Live Activity hiển thị trên Lock Screen và trong overlay pill của Stage Manager. iPad không có Dynamic Island (không có notch), nên bạn cần chú trọng thiết kế Lock Screen layout, đó là surface chính trên iPad.

Làm sao để test Dynamic Island mà không cần iPhone Pro?

Dùng simulator iPhone 15 Pro/16 Pro trở lên trong Xcode 26, chọn Preview với modifier .dynamicIsland(.compact/.expanded/.minimal) để xem cả bốn state ngay trong Xcode. Tuy nhiên animation transition trên simulator khác device thật, nên trước khi ship phải test trên phần cứng.

Live Activity kéo dài tối đa bao lâu?

Mặc định 8 giờ (12 giờ cho sports events trong iOS 26). Sau đó hệ thống tự end. Nếu event của bạn dài hơn (ví dụ chuyến bay xuyên đại dương), cân nhắc restart một activity mới khi gần hết giới hạn, hoặc dùng scheduled activity mới của iOS 26.

Khác biệt giữa Live Activity và Widget là gì?

Widget là bề mặt vĩnh viễn user cài lên Home Screen, cập nhật theo TimelineProvider và tồn tại vô thời hạn. Live Activity gắn với một event thời gian thực, xuất hiện tự động trên Lock Screen/Dynamic Island khi event bắt đầu, cập nhật qua ActivityKit hoặc APNs, và biến mất khi event kết thúc.

Có cần user cấp quyền để dùng Live Activity không?

Không cần dialog permission. User chỉ cần bật toggle "Live Activities" trong Settings của app bạn (mặc định ON). Bạn nên check ActivityAuthorizationInfo().areActivitiesEnabled trước khi request để tránh throw error khi user đã tự tắt.

Broadcast Channel khác gì so với push per-token thông thường?

Push per-token gửi một request tới một device, phù hợp update cá nhân (delivery). Broadcast Channel gửi một payload duy nhất, APNs fan-out tới mọi device subscribe channel, phù hợp scale cho sports, election, event công cộng. Cost và độ trễ giảm hàng nghìn lần khi có nhiều subscriber cùng lúc.

Hiroshi Sato
Về Tác Giả Hiroshi Sato

Apple Platforms specialist building for iOS, macOS, visionOS, and the occasional watchOS app nobody asked for.