Loading...
Loading...
Transfer app data between platforms using AppMigrationKit. Use when implementing one-time data migration from Android or other platforms to iOS, managing cross-platform transfer sessions with AppMigrationExtension, packaging and archiving user data for export, importing resources on the destination device, tracking transfer progress, handling migration errors, or building onboarding flows that import existing user data.
npx skill4agent add dpearson2699/swift-ios-skills appmigrationkitBeta-sensitive. AppMigrationKit is new in iOS 26 and may change before GM. Re-check current Apple documentation before relying on specific API details.
AppMigrationExtensionMigrationStatus.importStatus| Type | Role |
|---|---|
| Protocol for the app extension entry point |
| Protocol for exporting files via archiver |
| Simplified export protocol (no custom options) |
| Protocol for importing files on the destination |
| Streams files into the export archive |
| Access to the containing app's data directories |
| Check import result from the containing app |
| Identifies the other device's platform (e.g., |
| Identifies the source app by store and bundle ID |
| Test-only actor for validating export/import logic |
com.apple.developer.app-migration.data-container-access<key>com.apple.developer.app-migration.data-container-access</key>
<array>
<string>com.example.myapp</string>
</array>ResourcesExportingWithOptionsResourcesExportingResourcesImportingAppMigrationExtensionappContainerimport AppMigrationKit
struct MyMigrationExtension: ResourcesExportingWithOptions, ResourcesImporting {
// Access the containing app's directories
let container = appContainer
// container.bundleIdentifier -- app's bundle ID
// container.containerRootDirectory -- root of the app container
// container.documentsDirectory -- Documents/
// container.applicationSupportDirectory -- Application Support/
}MigrationDataContainercontainerRootDirectorydocumentsDirectoryapplicationSupportDirectoryURLResourcesExportingWithOptionsResourcesExportingexportResources(to:request:)ResourcesArchiverMigrationRequestWithOptionsstruct 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
}
}resourcesSizeEstimateresourcesVersionresourcesCompressibletruefunc 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:)ResourcesArchiverMigrationRequestWithOptionsdestinationPlatformMigrationPlatformif request.destinationPlatform == .android {
// Export in a format the Android app expects
}MigrationPlatform.androidMigrationPlatform("customPlatform")ResourcesImportingimportResources(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)
}ResourcesImportRequestsourceAppIdentifierMigrationAppIdentifierplatform.androidbundleIdentifierstoreIdentifier.googlePlayimport 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.importStatusnilclearImportStatus().success.failure(any Error)ProgressresourcesImportProgresscompletedUnitCountvar resourcesImportProgress: Progress {
Progress(totalUnitCount: 100)
}
func importResources(
at importedDataURL: URL,
request: ResourcesImportRequest
) async throws {
let progress = resourcesImportProgress
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)
progress.completedUnitCount += increment
}
progress.completedUnitCount = 100
}AppMigrationTesterimport 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)
}DeviceToDeviceExportPropertiesuncompressedBytescompressedBytessizeEstimateversion// 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-accessResourcesExportingWithOptionsResourcesExportingResourcesImportingresourcesSizeEstimateresourcesVersionappendItemResourcesArchiverMigrationStatus.importStatusclearImportStatus()AppMigrationTestersourceVersion