关于 appmigrationkit
appmigrationkit 是面向应用开发者的指南,介绍如何通过 AppMigrationKit 在设备设置或引导流程中传输应用文件。系统负责设备间传输,应用扩展实现导出和导入。
主要功能
- 01实现资源导出与导入扩展
- 02将文件流式写入传输归档
- 03按目标平台调整导出内容
- 04检查迁移状态并跟踪导入进度
- 05使用 AppMigrationTester 验证迁移逻辑
适用场景
- 01在设备设置期间迁移应用数据
- 02将应用文件从 Android 导入
- 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 通过三个层级运行:
- 应用扩展 -- 一种符合
AppMigrationExtension的类型,由系统在迁移期间调用。它负责处理数据导出和导入。 - 系统协调 -- OS 管理设备间会话、传输和调度。扩展无法控制自身的运行时机。
- 宿主应用 -- 迁移完成后,应用会在首次启动时检查
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处理有版本的数据格式