mcpField
A
appmigrationkit

appmigrationkit

github.com/dpearson2699开发1.2k3.1k 次安装13 周前更新
代码仓库

关于 appmigrationkit

appmigrationkit 是面向应用开发者的指南,介绍如何通过 AppMigrationKit 在设备设置或引导流程中传输应用文件。系统负责设备间传输,应用扩展实现导出和导入。

主要功能

  1. 01实现资源导出与导入扩展
  2. 02将文件流式写入传输归档
  3. 03按目标平台调整导出内容
  4. 04检查迁移状态并跟踪导入进度
  5. 05使用 AppMigrationTester 验证迁移逻辑

适用场景

  1. 01在设备设置期间迁移应用数据
  2. 02将应用文件从 Android 导入
  3. 03通过单元测试检查导出与导入流程

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill appmigrationkit

安装后在 Claude Code 中输入/appmigrationkit确认已加载。

附带文件evals/evals.jsonreferences/appmigrationkit-patterns.md

详细介绍

AppMigrationKit

用于应用资源的一次性跨平台数据传输。支持应用在设备设置或引导流程中,将数据导出到其他平台,或从其他平台(例如 Android)导入数据。AppMigrationKit API 适用于 iOS 26.0+ / iPadOS 26.0+;数据容器权利适用于 iOS 26.1+ / iPadOS 26.1+ / Mac Catalyst 26.1+。Swift 6.3。

对 Beta 版本敏感。 AppMigrationKit 在 iOS 26 中首次推出,正式发布前可能会有变更。 依赖具体 API 细节前,请重新查阅 Apple 当前的文档。

AppMigrationKit 采用应用扩展模型。系统负责协调设备间的数据传输。应用提供一个符合导出和导入协议的扩展,系统会在适当的时机调用该扩展。应用本身不会管理设备间的网络连接。

目录

架构概览

AppMigrationKit 通过三个层级运行:

  1. 应用扩展 -- 一种符合 AppMigrationExtension 的类型,由系统在迁移期间调用。它负责处理数据导出和导入。
  2. 系统协调 -- OS 管理设备间会话、传输和调度。扩展无法控制自身的运行时机。
  3. 宿主应用 -- 迁移完成后,应用会在首次启动时检查 MigrationStatus.importStatus,以确定是否发生了迁移以及迁移是否成功。

关键类型:

类型作用
AppMigrationExtension应用扩展入口点所遵循的协议
ResourcesExportingWithOptions通过归档器导出文件的协议
ResourcesExporting简化的导出协议(不支持自定义选项)
ResourcesImporting在目标设备上导入文件的协议
ResourcesArchiver将文件流式写入导出归档
MigrationDataContainer访问宿主应用数据目录的接口
MigrationStatus从宿主应用检查导入结果
MigrationPlatform标识另一台设备的平台(例如 .android)
MigrationAppIdentifier通过商店和 Bundle ID 标识源应用
AppMigrationTester用于验证导出/导入逻辑的仅供测试使用的 actor

设置和权利

权利

应用扩展需要 com.apple.developer.app-migration.data-container-access 权利。其值是一个只包含宿主应用 Bundle ID 的字符串数组:

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

其他值均无效。此权利允许扩展在导出期间读取宿主应用的数据容器,并在导入期间写入该容器。此权利适用于 iOS 26.1+、iPadOS 26.1+ 和 Mac Catalyst 26.1+,而 AppMigrationKit 的核心 API 适用于 iOS 26.0+ 和 iPadOS 26.0+。

扩展目标

在 Xcode 项目中添加一个新的 App Extension 目标。该扩展需符合一个或多个迁移协议(ResourcesExportingWithOptions、ResourcesExporting、ResourcesImporting)。

应用迁移扩展

扩展入口点符合 AppMigrationExtension。迁移期间,系统会阻止启动宿主应用及其其他扩展,以确保数据只能由扩展独占访问。

访问数据容器

扩展通过 appContainer 访问宿主应用的文件:

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's bundle ID
        // container.containerRootDirectory -- root of the app container
        // container.documentsDirectory    -- Documents/
        // container.applicationSupportDirectory -- Application Support/
    }
}

MigrationDataContainer 提供 containerRootDirectory、documentsDirectory 和 applicationSupportDirectory,这些 URL 值指向宿主应用沙盒中的目录。

导出资源

遵循 ResourcesExportingWithOptions(不使用自定义选项时遵循 ResourcesExporting),以打包文件供传输。系统会调用 exportResources(to:request:),并传入一个 ResourcesArchiver 和一个 MigrationRequestWithOptions。

声明导出属性
struct MyMigrationExtension: ResourcesExportingWithOptions {
    typealias OptionsType = MigrationDefaultSupportedOptions

    var resourcesSizeEstimate: Int {
        // Return estimated total bytes of exported data
        calculateExportSize()
    }

    var resourcesVersion: String {
        "1.0"
    }

    var resourcesCompressible: Bool {
        true  // Let the system compress during transport
    }
}
  • resourcesSizeEstimate -- 预估总字节数。系统会将其用于进度界面和可用空间检查。
  • resourcesVersion -- 格式版本字符串。导入端会收到该值,以处理有版本的数据格式。
  • resourcesCompressible -- 为 true 时,归档器可以在传输期间压缩文件。
实现导出
func exportResources(
    to archiver: sending ResourcesArchiver,
    request: MigrationRequestWithOptions<MigrationDefaultSupportedOptions>
) async throws {
    let docsDir = appContainer.documentsDirectory

    // Check destination platform if needed
    if request.destinationPlatform == .android {
        // Platform-specific export logic
    }

    // Append files one at a time -- make continuous progress
    let userDataURL = docsDir.appending(path: "user_data.json")
    try await archiver.appendItem(at: userDataURL)

    // Append with a custom archive path
    let settingsURL = docsDir.appending(path: "settings.plist")
    try await archiver.appendItem(at: settingsURL, pathInArchive: "preferences/settings.plist")

    // Append a directory
    let photosDir = docsDir.appending(path: "photos")
    try await archiver.appendItem(at: photosDir, pathInArchive: "media/photos")
}

归档器会增量流式传输文件。每当资源准备就绪时,重复调用 appendItem(at:pathInArchive:)。如果系统认为扩展程序没有响应,可能会终止它,因此请避免两次追加调用之间间隔过长。

取消

ResourcesArchiver 会自动处理任务取消,并抛出取消错误。不要捕获这些错误,否则系统会终止扩展程序。

迁移平台

MigrationRequestWithOptions 通过 MigrationPlatform 值提供 destinationPlatform。使用它来调整导出的数据:

if request.destinationPlatform == .android {
    // Export in a format the Android app expects
}

MigrationPlatform 提供 .android 静态常量。可以通过 MigrationPlatform("customPlatform") 创建自定义平台。

导入资源

遵循 ResourcesImporting,以在目标设备上接收传输的文件。系统会在应用安装后、应用可启动前调用 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        -- e.g., .android
        // sourceApp.bundleIdentifier -- source app's bundle ID
        // sourceApp.storeIdentifier  -- e.g., .googlePlay

        // Copy imported files into the app container
        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")
            )
        }
    }
}
导入期间的错误处理

导入出错时,系统会清除所在应用的数据容器,以防止出现不完整状态。不过,不会清除应用组容器。导入实现应在写入导入内容前清除任何应用组容器:

func importResources(
    at importedDataURL: URL,
    request: ResourcesImportRequest
) async throws {
    // Clear shared app group data first
    let groupURL = FileManager.default.containerURL(
        forSecurityApplicationGroupIdentifier: "group.com.example.myapp"
    )
    if let groupURL {
        try? FileManager.default.removeItem(at: groupURL.appending(path: "shared_data"))
    }

    // Then import
    try await performImport(from: importedDataURL)
}
来源应用标识符

ResourcesImportRequest 通过 MigrationAppIdentifier 提供 sourceAppIdentifier,其中包含三个属性:

  • platform -- 来源设备的平台(例如 .android)
  • bundleIdentifier -- 来源应用的包标识符
  • storeIdentifier -- 应用商店(例如 .googlePlay)

迁移状态

迁移完成后,所在应用会在首次启动时检查结果:

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(),以免后续启动时再次显示通知。
  • 此枚举有两个枚举值:.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
        }
        importProgress.completedUnitCount = 100
    }
}

测试

AppMigrationTester 是仅用于测试的 Actor,用于在由所在应用托管的单元测试中验证迁移逻辑。不要在生产环境中使用它。

import Testing
import AppMigrationKit

@Test func testExportImportRoundTrip() async throws {
    let tester = try await AppMigrationTester(platform: .android)

    // Export
    let result = try await tester.exportController.exportResources(
        request: nil, progress: nil
    )
    #expect(result.exportProperties.uncompressedBytes > 0)

    // Import the exported data
    try await tester.importController.importResources(
        from: result.extractedResourcesURL,
        importRequest: nil, progress: nil
    )
    try await tester.importController.registerImportCompletion(with: .success)
}

结果中的 DeviceToDeviceExportProperties 提供 uncompressedBytes、 compressedBytes(若不可压缩则为 nil)、sizeEstimate 和 version。

更多测试模式请参阅 references/appmigrationkit-patterns.md。

常见错误

不要:捕获 ResourcesArchiver 抛出的取消错误
// WRONG -- system kills the extension if cancellation is swallowed
func exportResources(to archiver: sending ResourcesArchiver, request: ...) async throws {
    do {
        try await archiver.appendItem(at: fileURL)
    } catch is CancellationError {
        // Swallowing this causes termination
    }
}

// CORRECT -- let cancellation propagate
func exportResources(to archiver: sending ResourcesArchiver, request: ...) async throws {
    try await archiver.appendItem(at: fileURL)
}
不要:让归档器的追加调用之间间隔过长
// WRONG -- system may assume the extension is hung and terminate it
func exportResources(to archiver: sending ResourcesArchiver, request: ...) async throws {
    let allFiles = gatherAllFiles()  // Takes 30 seconds
    for file in allFiles {
        try await archiver.appendItem(at: file)
    }
}

// CORRECT -- interleave file preparation with archiving
func exportResources(to archiver: sending ResourcesArchiver, request: ...) async throws {
    for file in knownFilePaths() {
        try await archiver.appendItem(at: file)
    }
}
不要:在导出期间将文件转换为中间格式
// WRONG -- may exhaust disk space creating temporary copies
func exportResources(to archiver: sending ResourcesArchiver, request: ...) async throws {
    let converted = try convertToJSON(originalDatabase)  // Doubles disk usage
    try await archiver.appendItem(at: converted)
}

// CORRECT -- export files as-is, convert on import side if needed
func exportResources(to archiver: sending ResourcesArchiver, request: ...) async throws {
    try await archiver.appendItem(at: originalDatabase)
}
不要:在导入错误恢复期间忽略应用组容器
// WRONG -- system clears app container but not app groups on error
func importResources(at url: URL, request: ResourcesImportRequest) async throws {
    try writeToAppGroup(data)
    try writeToAppContainer(data)  // If this throws, app group has stale data
}

// CORRECT -- clear app group data before importing
func importResources(at url: URL, request: ResourcesImportRequest) async throws {
    try clearAppGroupData()
    try writeToAppGroup(data)
    try writeToAppContainer(data)
}
不要:处理完导入状态后忘记清除
// WRONG -- migration UI shows every launch
if let status = MigrationStatus.importStatus {
    showMigrationResult(status)
    // Missing clearImportStatus()
}

// CORRECT
if let status = MigrationStatus.importStatus {
    showMigrationResult(status)
    MigrationStatus.clearImportStatus()
}

审查清单

  • 已添加扩展目标,并配置 com.apple.developer.app-migration.data-container-access 权限
  • 权限数组中恰好包含一个字符串:所属应用的 bundle 标识符
  • 扩展遵循 ResourcesExportingWithOptions 或 ResourcesExporting,以支持导出
  • 扩展遵循 ResourcesImporting,以支持导入
  • resourcesSizeEstimate 返回合理的字节数估算值
  • 已设置 resourcesVersion,并将在导入时检查其格式兼容性
  • 导出时增量调用 appendItem,避免长时间暂停
  • 不捕获 ResourcesArchiver 返回的取消错误
  • 导入前清空应用组容器
  • 所属应用在首次启动时检查 MigrationStatus.importStatus
  • 处理迁移结果后调用 clearImportStatus()
  • 在单元测试中使用 AppMigrationTester 验证导出和导入
  • 导出时按原样导出文件,不进行中间格式转换
  • 使用导入请求中的 sourceVersion 处理有版本的数据格式

参考资料

评论

支持 Markdown