mcpField
A
appmigrationkit

appmigrationkit

github.com/dpearson2699開発1.2k3.1k 回インストール13 週間前に更新
リポジトリ

appmigrationkit について

appmigrationkitは、デバイスの設定やオンボーディング時にAppMigrationKitでアプリのファイルを移行する開発者向けガイドです。デバイス間転送はシステムが管理し、アプリ拡張がエクスポートとインポートを実装します。

主な機能

  1. 01リソースのエクスポートとインポート拡張の実装
  2. 02転送アーカイブへのファイルのストリーミング
  3. 03移行先プラットフォームに応じたデータ調整
  4. 04移行状態の確認とインポート進捗の追跡
  5. 05AppMigrationTesterによる移行ロジックの検証

活用シーン

  1. 01デバイス設定時のアプリデータ移行
  2. 02Androidからのアプリファイルのインポート
  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。

ベータ版に関する注意。 AppMigrationKit は iOS 26 で新たに導入されたため、GM までに変更される可能性があります。特定の API の詳細を前提とする前に、最新の Apple ドキュメントを再確認してください。

AppMigrationKit はアプリ拡張モデルを使用します。デバイス間の転送はシステムが調整します。アプリはエクスポートおよびインポートのプロトコルに準拠する拡張機能を提供し、システムは適切なタイミングでその拡張機能を呼び出します。アプリ自体がデバイス間のネットワーク接続を管理することはありません。

目次

アーキテクチャの概要

AppMigrationKit は次の 3 つのレイヤーで動作します。

  1. アプリ拡張機能 -- 移行中にシステムが呼び出す、AppMigrationExtension に準拠する型です。データのエクスポートとインポートを処理します。
  2. システムによる調整 -- OS がデバイス間のセッション、転送、スケジュールを管理します。拡張機能は実行されるタイミングを制御しません。
  3. 拡張機能を含むアプリ -- 移行の完了後、アプリは初回起動時に MigrationStatus.importStatus を確認し、移行が行われたか、また成功したかを判定します。

主な型:

型役割
AppMigrationExtensionアプリ拡張機能のエントリーポイント用プロトコル
ResourcesExportingWithOptionsアーカイバーを介してファイルをエクスポートするプロトコル
ResourcesExporting簡易エクスポートプロトコル(カスタムオプションなし)
ResourcesImporting移行先でファイルをインポートするプロトコル
ResourcesArchiverファイルをエクスポートアーカイブにストリーミングする機能
MigrationDataContainer拡張機能を含むアプリのデータディレクトリへのアクセス
MigrationStatus拡張機能を含むアプリからインポート結果を確認
MigrationPlatformもう一方のデバイスのプラットフォームを識別(例:.android)
MigrationAppIdentifierストアとバンドル ID によってソースアプリを識別
AppMigrationTesterエクスポート/インポートロジックを検証するテスト専用アクター

セットアップとエンタイトルメント

エンタイトルメント

アプリ拡張機能には 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>

ほかの値は無効です。このエンタイトルメントにより、エクスポート中は拡張機能が拡張機能を含むアプリのデータコンテナを読み取ることができ、インポート中は書き込むことができます。エンタイトルメント自体は iOS 26.1+、iPadOS 26.1+、および Mac Catalyst 26.1+ で利用できますが、AppMigrationKit のコア API は iOS 26.0+ と iPadOS 26.0+ で利用できます。

拡張機能ターゲット

Xcode プロジェクトに新しい App Extension ターゲットを追加します。この拡張機能は、移行プロトコル(ResourcesExportingWithOptions、ResourcesExporting、ResourcesImporting)の 1 つ以上に準拠します。

アプリ移行拡張機能

拡張機能のエントリーポイントは 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 は、拡張機能を含むアプリのサンドボックス内を指す URL 値として、containerRootDirectory、documentsDirectory、および applicationSupportDirectory を提供します。

リソースのエクスポート

ResourcesExportingWithOptions(カスタムオプションが不要な場合はResourcesExporting)に準拠して、転送するファイルをパッケージ化します。システムはResourcesArchiverとMigrationRequestWithOptionsを使ってexportResources(to:request:)を呼び出します。

エクスポートプロパティの宣言
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 -- 合計バイト数の推定値です。システムは進捗状況のUIと空き容量の確認に使用します。
  • 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:)を繰り返し呼び出します。処理が停止しているように見えると、システムがエクステンションを終了する場合があるため、append呼び出しの間隔を長くしないでください。

キャンセル

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")
            )
        }
    }
}
インポート中のエラー処理

インポートエラーが発生すると、システムは部分的な状態が残らないよう、対象アプリのデータコンテナを消去します。ただし、App Groupコンテナは消去されません。インポート実装では、インポートした内容を書き込む前に、App Groupコンテナを消去してください。

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は、次の3つのプロパティを持つ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)の2つのケースがあります。

進捗状況の追跡

インポート側は、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は、対象アプリがホストする単体テストで移行ロジックを検証するための、テスト専用のアクターです。本番環境では使用しないでください。

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)
}
やってはいけないこと:アーカイバーのappend呼び出しの間隔を長くする
// 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)
}
やってはいけないこと:インポートエラーの復旧時にApp Groupコンテナを無視する
// 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 エンタイトルメントを含む拡張機能ターゲットを追加している
  • エンタイトルメント配列に、コンテナアプリのバンドル識別子のみが文字列として1つ含まれている
  • 拡張機能がエクスポート用に ResourcesExportingWithOptions または ResourcesExporting に準拠している
  • 拡張機能がインポート用に ResourcesImporting に準拠している
  • resourcesSizeEstimate が妥当なバイト数の見積もりを返している
  • resourcesVersion が設定され、フォーマットの互換性を確認するためにインポート時にチェックされる
  • エクスポートで、長い間隔を空けずに appendItem を逐次呼び出している
  • ResourcesArchiver からのキャンセルエラーを捕捉していない
  • 新しいデータを書き込む前に、インポートでアプリグループのコンテナをクリアしている
  • コンテナアプリが初回起動時に MigrationStatus.importStatus を確認している
  • 移行結果の処理後に clearImportStatus() を呼び出している
  • エクスポートとインポートを検証するために、ユニットテストで AppMigrationTester を使用している
  • エクスポート側で中間フォーマットに変換せず、ファイルをそのままエクスポートしている
  • バージョン管理されたデータ形式を処理するために、インポートリクエストの sourceVersion を使用している

参考資料

コメント