appmigrationkit

appmigrationkit

熱門

使用 AppMigrationKit 跨平台轉移 App 資料。適用於實作系統協調調度的 iOS 與 Android(或其他平台)間一次性轉移、開發 AppMigrationExtension、利用 ResourcesArchiver 打包可轉移資源、在目標裝置上匯入資源、回報匯入進度、處理轉移錯誤與 App Group 清除作業、檢查 MigrationStatus,或使用 AppMigrationTester 測試轉移程式碼。

967星標
49分支
更新於 2026/7/31
SKILL.md
唯讀
名稱
appmigrationkit
描述

使用 AppMigrationKit 跨平台轉移 App 資料。適用於實作系統協調調度的 iOS 與 Android(或其他平台)間一次性轉移、開發 AppMigrationExtension、利用 ResourcesArchiver 打包可轉移資源、在目標裝置上匯入資源、回報匯入進度、處理轉移錯誤與 App Group 清除作業、檢查 MigrationStatus,或使用 AppMigrationTester 測試轉移程式碼。

AppMigrationKit

App 資源的一次性跨平台資料轉移方案。讓 App 能在裝置設定或開機導覽(onboarding)期間,匯出資料至另一個平台(例如 Android)或從中匯入資料。AppMigrationKit API 適用於 iOS 26.0+ / iPadOS 26.0+;資料容器權限(data-container entitlement)則適用於 iOS 26.1+ / iPadOS 26.1+ / Mac Catalyst 26.1+。Swift 6.3。

Beta 測試版注意事項:AppMigrationKit 為 iOS 26 新增功能,在 GM 正式版釋出前可能有所變動。在依賴特定 API 細節前,請重新確認最新的 Apple 官方說明文件。

AppMigrationKit 採用 App Extension(應用程式擴充套件)架構模型。系統會協調調度裝置之間的轉移作業。App 需提供符合匯出與匯入協定(protocol)的 Extension,系統會在適當的時機呼叫該 Extension。App 本身絕不直接管理裝置間的網絡連線。

目錄

架構總覽

AppMigrationKit 分為三個層級運作:

  1. App extension -- 符合 AppMigrationExtension 協定的型別,系統會在轉移期間呼叫它,負責處理資料匯出與匯入。
  2. 系統協調調度(System orchestration) -- 由 OS 管理裝置對裝置的連線階段(session)、傳輸與排程。Extension 無法控制自身的執行時機。
  3. 主 App(Containing app) -- 轉移完成後,App 在首次啟動時會檢查 MigrationStatus.importStatus,以確認是否發生過轉移以及轉移是否成功。

主要型別:

型別 角色
AppMigrationExtension App extension 進入點的協定
ResourcesExportingWithOptions 透過封檔器(archiver)匯出檔案的協定
ResourcesExporting 簡化版匯出協定(不含自訂選項)
ResourcesImporting 在目標裝置匯入檔案的協定
ResourcesArchiver 將檔案串流寫入匯出封檔
MigrationDataContainer 存取主 App 的資料目錄
MigrationStatus 從主 App 檢查匯入結果
MigrationPlatform 識別另一台裝置的平台(例如 .android)
MigrationAppIdentifier 透過商店與 Bundle ID 識別來源 App
AppMigrationTester 僅供測試使用、用來驗證匯出/匯入邏輯的 Actor

設定與 Entitlements

Entitlement

App extension 需要配置 com.apple.developer.app-migration.data-container-access Entitlement。其值為僅包含單一元素的字串陣列,內容為主 App 的 Bundle Identifier:

<key>com.apple.developer.app-migration.data-container-access</key>
<array>
    <string>com.example.myapp</string>
</array>

其他值皆無效。此 Entitlement 能賦予 Extension 在匯出期間對主 App 資料容器的讀取權限,以及在匯入期間的寫入權限。雖然 AppMigrationKit 核心 API 支援 iOS 26.0+ 和 iPadOS 26.0+,但此 Entitlement 本身僅支援 iOS 26.1+、iPadOS 26.1+ 及 Mac Catalyst 26.1+。

Extension Target

在 Xcode 專案中新增 App Extension target。該 Extension 需符合一個或多個轉移協定(ResourcesExportingWithOptions、ResourcesExporting、ResourcesImporting)。

App Migration Extension

Extension 的進入點符合 AppMigrationExtension。轉移期間,系統會阻止啟動主 App 及其其他 Extension,以確保獨佔資料存取權。

存取資料容器

Extension 透過 appContainer 存取主 App 的檔案:

import AppMigrationKit

struct MyMigrationExtension: ResourcesExporting {
    var resourcesSizeEstimate: Int { estimateTotalExportSize() }
    var resourcesVersion: String { "1.0" }
    var resourcesCompressible: Bool { true }

    func exportResources(
        to archiver: sending ResourcesArchiver,
        request: MigrationRequest
    ) async throws {
        let container = appContainer

        // container.bundleIdentifier     -- App 的 Bundle ID
        // container.containerRootDirectory -- App 容器根目錄
        // container.documentsDirectory    -- Documents/
        // container.applicationSupportDirectory -- Application Support/
    }
}

MigrationDataContainer 提供 containerRootDirectory、documentsDirectory 和 applicationSupportDirectory,這些皆為指向主 App 沙盒內部的 URL 值。

匯出資源

實作 ResourcesExportingWithOptions(若無自訂選項則實作 ResourcesExporting)以打包傳輸檔案。系統會傳入 ResourcesArchiver 與 MigrationRequestWithOptions 並呼叫 exportResources(to:request:)。

宣告匯出屬性

struct MyMigrationExtension: ResourcesExportingWithOptions {
    typealias OptionsType = MigrationDefaultSupportedOptions

    var resourcesSizeEstimate: Int {
        // 回傳預估的匯出資料總 Bytes 數
        calculateExportSize()
    }

    var resourcesVersion: String {
        "1.0"
    }

    var resourcesCompressible: Bool {
        true  // 允許系統在傳輸過程中進行壓縮
    }
}
  • resourcesSizeEstimate -- 預估的總位元組數(Bytes)。系統會以此作為進度 UI 展示與剩餘空間檢查的依據。
  • resourcesVersion -- 格式版本字串。匯入端會接收此資訊以處理具備版本控管的資料格式。
  • resourcesCompressible -- 當值為 true 時,封檔器可在傳輸過程中壓縮檔案。

實作匯出

func exportResources(
    to archiver: sending ResourcesArchiver,
    request: MigrationRequestWithOptions<MigrationDefaultSupportedOptions>
) async throws {
    let docsDir = appContainer.documentsDirectory

    // 必要時檢查目標平台
    if request.destinationPlatform == .android {
        // 平台專屬的匯出邏輯
    }

    // 逐一附加檔案 -- 保持持續性的處理進度
    let userDataURL = docsDir.appending(path: "user_data.json")
    try await archiver.appendItem(at: userDataURL)

    // 使用自訂的封檔路徑進行附加
    let settingsURL = docsDir.appending(path: "settings.plist")
    try await archiver.appendItem(at: settingsURL, pathInArchive: "preferences/settings.plist")

    // 附加整個目錄
    let photosDir = docsDir.appending(path: "photos")
    try await archiver.appendItem(at: photosDir, pathInArchive: "media/photos")
}

封檔器會以串流方式漸進式打包檔案。當每個資源準備就緒時,重複呼叫 appendItem(at:pathInArchive:)。若 Extension 看起來像卡住無回應,系統可能會將其終止,因此請避免在多次 append 呼叫之間產生過長的空檔。

取消機制

ResourcesArchiver 透過拋出取消錯誤(cancellation errors)自動處理任務取消。請勿捕捉(catch)這些錯誤 -- 否則系統會直接強制關閉(kill)Extension。

轉移平台

MigrationRequestWithOptions 將 destinationPlatform 開放為 MigrationPlatform 型別的值。可用此客製化匯出的資料:

if request.destinationPlatform == .android {
    // 以 Android App 預期的格式匯出
}

MigrationPlatform 提供靜態常數 .android。亦可使用 MigrationPlatform("customPlatform") 建立自訂平台。

匯入資源

實作 ResourcesImporting 以在目標裝置上接收傳輸的檔案。系統會在安裝 App 後、但 App 尚未可啟動之前呼叫 importResources(at:request:)。

struct MyMigrationExtension: ResourcesImporting {
    func importResources(
        at importedDataURL: URL,
        request: ResourcesImportRequest
    ) async throws {
        let sourceVersion = request.sourceVersion
        let sourceApp = request.sourceAppIdentifier

        // sourceApp.platform        -- 例如 .android
        // sourceApp.bundleIdentifier -- 來源 App 的 Bundle ID
        // sourceApp.storeIdentifier  -- 例如 .googlePlay

        // 將匯入的檔案複製到 App 容器中
        let docsDir = appContainer.documentsDirectory

        let userData = importedDataURL.appending(path: "user_data.json")
        if FileManager.default.fileExists(atPath: userData.path()) {
            try FileManager.default.copyItem(
                at: userData,
                to: docsDir.appending(path: "user_data.json")
            )
        }
    }
}

匯入期間的錯誤處理

發生匯入錯誤時,系統會清空主 App 的資料容器以避免留下殘缺狀態。然而,App Group 容器並不會被自動清空。匯入實作應在寫入匯入內容前先自行清空所有 App Group 容器:

func importResources(
    at importedDataURL: URL,
    request: ResourcesImportRequest
) async throws {
    // 先清除共享的 App Group 資料
    let groupURL = FileManager.default.containerURL(
        forSecurityApplicationGroupIdentifier: "group.com.example.myapp"
    )
    if let groupURL {
        try? FileManager.default.removeItem(at: groupURL.appending(path: "shared_data"))
    }

    // 接著執行匯入
    try await performImport(from: importedDataURL)
}

來源 App 識別碼

ResourcesImportRequest 提供 sourceAppIdentifier 作為 MigrationAppIdentifier,包含三個屬性:

  • platform -- 來源裝置的平台(例如 .android)
  • bundleIdentifier -- 來源 App 的 Bundle Identifier
  • storeIdentifier -- App 商店(例如 .googlePlay)

轉移狀態

轉移完成後,主 App 會在首次啟動時檢查結果:

import AppMigrationKit

func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
    if let status = MigrationStatus.importStatus {
        switch status {
        case .success:
            showMigrationSuccessUI()
            MigrationStatus.clearImportStatus()
        case .failure(let error):
            showMigrationFailureUI(error: error)
            MigrationStatus.clearImportStatus()
        }
    }
    return true
}
  • 若未進行任何轉移,MigrationStatus.importStatus 為 nil。
  • 處理完結果後請呼叫 clearImportStatus(),以避免在後續啟動時重複顯示通知。
  • 此列舉(enum)有兩個 Case:.success 與 .failure(any Error)。

進度追蹤

匯入端透過 resourcesImportProgress 開放 Progress 物件。系統會用它來向使用者展示傳輸進度。請在匯入過程中漸進式更新 completedUnitCount:

struct MyMigrationExtension: ResourcesImporting {
    private let importProgress = Progress(totalUnitCount: 100)

    var resourcesImportProgress: Progress { importProgress }

    func importResources(
        at importedDataURL: URL,
        request: ResourcesImportRequest
    ) async throws {
        let files = try FileManager.default.contentsOfDirectory(
            at: importedDataURL, includingPropertiesForKeys: nil
        )
        let increment = Int64(100 / max(files.count, 1))
        for file in files {
            try processFile(file)
            importProgress.completedUnitCount += increment
        }
        importProgres