WWDC Quick Look 💓 By SwiftGGTeam
What's new in Background Assets

What's new in Background Assets

元の動画を見る

ハイライト

iOS 17 の Background Assets には Essential Downloads が追加され、アプリは App Store のインストール中に必須リソースをダウンロードでき、ユーザーは初回起動時に待たされません。さらに xcrun backgroundassets-debug で Extension の各エントリーポイントをシミュレートできます。

主要内容

ユーザーが最も嫌がるのは、アプリを開いた直後に長いダウンロード進捗バーを見ることです。ゲームのステージ、動画講座、雑誌コンテンツ——大容量リソースを起動後にダウンロードすると体験が悪化します。

01:05)Background Assets フレームワーク(iOS 16.1 導入)は App Extension 経由でアプリが動いていない間にリソースをダウンロードします。今年は Essential Downloads が追加され、重要なリソースを App Store インストール中に完了できます。

基本アーキテクチャ

01:20)Background Assets は 2 つの部分で構成されます:

  • Background Assets フレームワーク:アプリ内でダウンロードをスケジュール
  • App Extension:アプリ未実行時にダウンロードロジックを実行

Extension は次の 3 つのタイミングで起動されます:アプリインストール時、アプリ更新時、バックグラウンドの定期チェック時。

Essential Downloads

10:35)Essential Downloads は今年の中核機能です。essential とマークされたリソースはアプリインストール中にダウンロードされ、App Store のダウンロード進捗と統合表示されます。必須ダウンロードが完了するまでユーザーはアプリを起動できません。

ゲームの最初のステージ、アプリのコアコンテンツなど、初回起動時に必須のリソースに適しています。

実行時制限

04:42)Extension には厳しいリソース制限があります:

  • メモリ:数 MB 程度。超過すると終了
  • 実行時間:デフォルトで 1 日数分。アプリの使用例頻度に応じて動的調整
  • 低電力モードやバックグラウンド更新オフ時は Extension は動作しない

デバッグツール

29:38xcrun backgroundassets-debug で Extension の各エントリーポイントを、実際のインストールやバックグラウンドイベントを待たずにシミュレートできます。

詳細

Info.plist の設定

16:00)アプリの Info.plist に必須キーを追加:

<key>BAManifestURL</key>
<string>https://your-cdn.com/manifest.json</string>

<key>BAInitialDownloadRestrictions</key>
<dict>
    <key>BADownloadAllowance</key>
    <integer>104857600</integer>
    <key>BAEssentialDownloadAllowance</key>
    <integer>52428800</integer>
    <key>BADownloadDomainAllowList</key>
    <array>
        <string>your-cdn.com</string>
    </array>
</dict>

<key>BAMaxInstallSize</key>
<integer>209715200</integer>

<key>BAEssentialMaxInstallSize</key>
<integer>104857600</integer>

キーポイント:

  • BAManifestURL:マニフェストファイルの URL
  • BADownloadAllowance:非必須リソースの総ダウンロードサイズ上限(バイト)
  • BAEssentialDownloadAllowance:必須リソースの総ダウンロードサイズ上限(バイト)
  • BAEssentialMaxInstallSize:必須リソースインストール後のディスク使用例量上限
  • BAMaxInstallSize:非必須リソースインストール後のディスク使用例量上限
  • BADownloadDomainAllowList:初回起動前にダウンロードを許可するドメインのホワイトリスト

Extension の実装

24:46)Background Assets Extension を作成:

import BackgroundAssets

class DownloadExtension: NSObject, BADownloaderExtension {
    
    func downloads(for request: BAContentRequest,
                   manifestURL: URL,
                   extensionInfo: BAAppExtensionInfo) -> Set<BADownload> {
        
        // マニフェストファイルを解析
        guard let manifestData = try? Data(contentsOf: manifestURL),
              let manifest = try? JSONDecoder().decode(Manifest.self, from: manifestData) else {
            return []
        }
        
        // マニフェストを App Group に保存
        saveManifestToAppGroup(manifest)
        
        var downloads: Set<BADownload> = []
        
        for session in manifest.sessions where !session.isDownloaded {
            let download = BAURLDownload(
                identifier: session.id,
                request: URLRequest(url: session.videoURL),
                essential: session.isEssential,
                fileSize: session.fileSize,
                applicationGroupIdentifier: "group.com.yourapp",
                priority: .default
            )
            downloads.insert(download)
        }
        
        return downloads
    }
    
    func backgroundDownload(_ download: BADownload,
                           didFinishDownloadingTo fileURL: URL) {
        // withExclusiveControl で排他的アクセスを保証
        BADownloadManager.shared.withExclusiveControl { control in
            guard let control = control else { return }
            
            // 一時場所から最終場所へファイルを移動
            do {
                let finalURL = self.finalURL(for: download)
                try FileManager.default.moveItem(at: fileURL, to: finalURL)
                
                // ローカル状態を更新
                self.markSessionAsDownloaded(download.identifier)
            } catch {
                print("Failed to move file: \(error)")
            }
        }
    }
    
    func backgroundDownload(_ download: BADownload, failedWithError error: Error) {
        // 必須ダウンロードが失敗した場合は、非必須として再度キューに入れる
        if let urlDownload = download as? BAURLDownload, urlDownload.isEssential {
            let nonEssentialDownload = urlDownload.removingEssential()
            try? BADownloadManager.shared.scheduleDownload(nonEssentialDownload)
        }
    }
}

キーポイント:

  • downloads(for:manifestURL:extensionInfo:) はダウンロード対象リソースの集合を返す
  • 必須ダウンロードはインストール/更新イベント中にのみスケジュール可能
  • withExclusiveControl でアプリと Extension の同時ファイル操作を防止
  • ダウンロード完了後すぐに最終位置へ移動
  • 必須ダウンロード失敗時は removingEssential() で非必須として再キュー

アプリ側の統合

19:31)アプリでダウンロードを管理:

import BackgroundAssets

class SessionManager: NSObject, BADownloadManagerDelegate {
    let downloadManager = BADownloadManager.shared
    
    override init() {
        super.init()
        downloadManager.delegate = self
    }
    
    func startDownload(for session: Session) {
        downloadManager.withExclusiveControl { control in
            guard let control = control else { return }
            
            // すでにダウンロードキューにあるか確認
            if let existingDownload = self.downloadManager.currentDownloads
                .first(where: { $0.identifier == session.id }) {
                // 存在する場合はフォアグラウンドに昇格してダウンロードを高速化
                self.downloadManager.startForegroundDownload(existingDownload)
            } else {
                // 新しいダウンロードを作成
                let download = BAURLDownload(
                    identifier: session.id,
                    request: URLRequest(url: session.videoURL),
                    essential: false,
                    fileSize: session.fileSize,
                    applicationGroupIdentifier: "group.com.yourapp",
                    priority: .default
                )
                
                try? self.downloadManager.scheduleDownload(download)
                self.downloadManager.startForegroundDownload(download)
            }
        }
    }
    
    // MARK: - BADownloadManagerDelegate
    
    func downloadManager(_ manager: BADownloadManager,
                        downloadDidProgress download: BADownload) {
        updateProgress(for: download.identifier, progress: download.progress)
    }
    
    func downloadManager(_ manager: BADownloadManager,
                        downloadDidFinish download: BADownload) {
        // ファイルは Extension 内で最終場所へ移動済み
        markSessionAsDownloaded(download.identifier)
    }
    
    func downloadManager(_ manager: BADownloadManager,
                        downloadDidFail download: BADownload,
                        withError error: Error) {
        print("Download failed: \(error)")
    }
}

キーポイント:

  • BADownloadManager はシングルトンでシステムスケジューラに直接接続
  • withExclusiveControl で Extension との排他制御を保証
  • startForegroundDownload でバックグラウンドダウンロードをフォアグラウンドに昇格し高速化
  • フォアグラウンド昇格はダウンロードを再開せず、中断位置から再開
  • フォアグラウンドダウンロードはネットワーク問題で即失敗(バックグラウンドのようにリトライしない)

Extension のデバッグ

29:38backgroundassets-debug でエントリーポイントをシミュレート:

# 接続済みデバイスを一覧表示
xcrun backgroundassets-debug list-devices

# アプリインストールイベントをシミュレート
xcrun backgroundassets-debug simulate app-install \
    --device <DEVICE_UUID> \
    --bundle-id com.yourapp

# アプリ更新イベントをシミュレート
xcrun backgroundassets-debug simulate app-update \
    --device <DEVICE_UUID> \
    --bundle-id com.yourapp

# バックグラウンドの定期チェックをシミュレート
xcrun backgroundassets-debug simulate periodic \
    --device <DEVICE_UUID> \
    --bundle-id com.yourapp

キーポイント:

  • デバイスは開発者モードが有効である必要がある
  • Bluetooth や Wi-Fi 経由でコマンド送信可能。USB 不要
  • シミュレートイベントは Extension の実行時間制限をリセット
  • man backgroundassets-debug で完全なドキュメントを確認

ファイル管理のベストプラクティス

09:44)ダウンロードファイルはデフォルトでパージ可能(purgeable)としてマーク:

// ダウンロードしたファイルを Caches ディレクトリに置く
let cachesURL = FileManager.default.urls(for: .cachesDirectory,
                                          in: .userDomainMask).first!
let finalURL = cachesURL.appendingPathComponent("downloads/\(identifier)")

// コピーではなく move 操作を使う
try FileManager.default.moveItem(at: tempURL, to: finalURL)

キーポイント:

  • ダウンロードファイルは Caches ディレクトリに配置。容量不足時にシステムが削除可能
  • copyItem ではなく moveItem を使い、パージ可能属性を維持
  • 変更や展開後のファイルはシステム追跡外となり、バックアップ容量を消費
  • アプリ起動時にファイル存在を確認し、なければ再ダウンロード

Essential Download のライフサイクル

11:21)必須ダウンロードのライフサイクル:

  1. ユーザーが App Store からアプリのインストールを要求
  2. システムが Info.plist の essential 設定を読み取り
  3. アプリインストール完了後、システムが Extension を起動
  4. Extension が必須と非必須のダウンロードの混合集合を返す
  5. 必須ダウンロードが即座に開始され、アプリインストール進捗と統合表示
  6. 必須ダウンロード完了後、アプリが起動可能に
  7. 非必須ダウンロードはバックグラウンドで継続

キーポイント:

  • 必須ダウンロードのファイルサイズは正確である必要がある。不正確だと失敗
  • ユーザーは App Store 設定でアプリ内コンテンツダウンロードを無効化可能
  • アプリは必須リソース未完了のケースに対応する必要がある
  • サーバーは HTTP Range リクエストをサポートし、レジューム可能にする

重要ポイント

1. コンテンツ型アプリで「開いてすぐ使える」体験を実現

やること:アプリのコアコンテンツ(ニュースアプリのホームデータ、教育アプリの第 1 レッスン、ゲームの最初の数ステージ)を Essential Downloads にマークし、初回起動ですぐ使えるようにする。

価値:App Store からダウンロード後にコンテンツ待ちが発生すると離脱率が高い。Essential Downloads でコンテンツをアプリと同時に届けられる。

始め方:Info.plist で BAEssentialDownloadAllowanceBAEssentialMaxInstallSize を設定し、Extension の downloads(for:) でコアリソースの essentialtrue に。

2. スマートなリソースプリロードシステムを構築

やること:バックグラウンドの定期チェックで、ユーザーがアプリを使っていない間に新コンテンツをプリロード。例:Wi-Fi 時にポッドキャストアプリが新エピソードを自動ダウンロード。

価値:Background Assets は使用例習慣に基づき Extension 実行をスマートにスケジュール。よく使うアプリはより多くの実行時間を得る。

始め方:Extension でマニフェストを解析し、サーバー最新とローカル済みを比較して更新用 BADownload 集合を返す。BADownloadManager.currentDownloads で重複スケジュールを回避。

3. ゲームアプリでステージのストリーミング読み込みを実装

やること:ステージを必須(最初の 3 ステージ)と任意(以降)に分け、必須はインストール時、以降はバックグラウンドでプリロード。

価値:ゲームアセットは巨大で、IPA に全部入れると App Store 制限を超える。Background Assets でステージをオンデマンドダウンロード可能。

始め方:各ステージのファイルサイズと URL を記録するリソースマニフェストを設計。初期ステージを essential、残りを非必須に。Extension でユーザー進行に応じて次に使うステージを優先ダウンロード。

4. オフラインファーストの動画学習アプリを実装

やること:オフライン視聴したい動画をユーザーがマークし、Background Assets がバックグラウンドでダウンロード、完了時に通知。

価値:Wi-Fi 環境でマーク、バックグラウンドで自動ダウンロード、地下鉄や飛行機で既にコンテンツが準備完了。

始め方:アプリ内で「ダウンロード待ち」リストを維持し、App Group で Extension と共有。Extension がバックグラウンド実行時にリストを読み取りダウンロードをスケジュール。起動時に withExclusiveControl で状態確認し UI 更新。

関連セッション

コメント

GitHub Issues · utterances