3/8

2026

App Groupの設定からデータ共有まで — Capability・Profile・実装パターン

#App Group#iOS#Swift#Provisioning Profile#WatchKitApp Groupの設定からデータ共有まで — Capability・Profile・実装パターン

App Groupの設定からデータ共有まで

PlayTimeにWatch Smart Stack1対応を追加したとき、iPhone本体アプリとWatch Widget Extension2のあいだでデータを共有する必要があった。iOSアプリは各プロセスがサンドボックス3で隔離されているため、普通にUserDefaultsやファイルを読み書きしても別プロセスからは見えない。

この壁を越えるのがApp Group4だ。この記事では、App Groupの仕組みから、Apple Developer Portalでの設定、Provisioning Profile5の再生成、そして実装パターンまでを一通りまとめる。

App Groupの仕組み

サンドボックスの制約

iOSアプリは1プロセスにつき1つのサンドボックスを持つ。サンドボックス内のファイルやUserDefaultsは、そのプロセスだけがアクセスできる。

┌─────────────────────┐  ┌─────────────────────┐
│  iPhone App          │  │  Watch Widget Ext    │
│  (sandbox A)         │  │  (sandbox B)         │
│                      │  │                      │
│  UserDefaults ──────────→ ❌ アクセス不可       │
│  Documents/   ──────────→ ❌ アクセス不可       │
└─────────────────────┘  └─────────────────────┘

Widget Extension、Watch App、Share Extension6など、メインアプリとは別プロセスで動くものはすべてこの制約を受ける。

共有コンテナ

App Groupを有効にすると、OSがグループ識別子(group.com.example.app)に紐づく共有コンテナを作成する。同じApp Groupに所属するプロセスは、このコンテナに対して読み書きできる。

┌─────────────────────┐  ┌─────────────────────┐
│  iPhone App          │  │  Watch Widget Ext    │
│  (sandbox A)         │  │  (sandbox B)         │
│         │            │  │         │            │
│         └───────┐    │  │    ┌────┘            │
└─────────────────│────┘  └────│─────────────────┘
                  ▼            ▼
         ┌──────────────────────────┐
         │  共有コンテナ              │
         │  group.com.example.app   │
         │                          │
         │  ✅ UserDefaults(suite)  │
         │  ✅ FileManager container│
         │  ✅ Core Data / Realm DB │
         └──────────────────────────┘

共有されるのはコンテナだけで、各プロセスのサンドボックス自体は引き続き隔離されている。つまり「共有したいデータだけを共有コンテナに置く」という設計になる。

💡 App Group識別子はgroup.で始まる必要がある。逆ドメイン形式が慣例: group.com.yourcompany.yourapp

Apple Developer PortalでCapabilityを有効化する

App Groupを使うには、まずApple Developer Portal7でBundle ID8にApp Groups Capabilityを追加する必要がある。

GUI手順

  1. Apple Developer Portal → Certificates, Identifiers & Profiles → Identifiers
  2. 対象のBundle IDを選択
  3. 「Capabilities」タブでApp Groupsにチェック
  4. 「Edit」→ App Group識別子を追加(group.com.example.app
  5. 「Save」

メインアプリ、Widget Extension、Watch App Extensionなど、共有に参加するすべてのBundle IDに対して同じ手順を繰り返す。1つでも漏れるとそのプロセスからは共有コンテナにアクセスできない。

fastlaneでのCLI手順

GUI操作が面倒な場合、fastlane9のCLIで設定できる。ただし2つのコマンドが必要で、それぞれ役割が異なる。

# ステップ1: App Groupコンテナを登録(Developer Portalに識別子を作成)
fastlane produce group -g group.com.example.app -n "MyApp Shared"

# ステップ2: App GroupコンテナをBundle IDに紐づけ
fastlane produce associate_group \
  -a com.example.app \
  --group-id group.com.example.app

# 他のBundle IDにも同じコンテナを紐づけ
fastlane produce associate_group \
  -a com.example.app.watchkitextension \
  --group-id group.com.example.app

⚠️ produce groupproduce associate_group別のコマンドで、両方とも必要produce groupはコンテナの登録(作成)のみ。associate_groupがBundle IDへの紐づけを行う。紐づけを忘れると、Capabilityが有効でもProfileにグループ情報が含まれない。

📝 fastlaneのこれらのコマンドはDeveloper Portalの内部API10を使うため、Apple ID + パスワード + 2FA11認証が要求される。ASC REST APIのJWTトークンでは認証できない。

App Store Connect REST APIでCapabilityを有効化する

CI/CD12パイプラインからプログラマティックにCapabilityを設定する場合、App Store Connect API13を使う。

認証

App Store Connect APIはJWT14ベースの認証を使う。

  1. App Store Connect → Users and Access → Integrations → App Store Connect API → Keys
  2. API Keyを作成(Admin権限推奨)
  3. Issuer ID、Key ID、.p8ファイルを取得
# JWTトークン生成(Ruby例)
TOKEN=$(ruby -e '
require "jwt"
key = OpenSSL::PKey::EC.new(File.read("AuthKey_XXXXXXXXXX.p8"))
payload = {
  iss: "YOUR_ISSUER_ID",
  iat: Time.now.to_i,
  exp: Time.now.to_i + 20 * 60,
  aud: "appstoreconnect-v1"
}
puts JWT.encode(payload, key, "ES256", { kid: "YOUR_KEY_ID" })
')

Capability有効化(ON/OFFのみ)

# 1. Bundle IDのリソースIDを取得
curl -H "Authorization: Bearer $TOKEN" \
  "https://api.appstoreconnect.apple.com/v1/bundleIds?filter[identifier]=com.example.app" \
  | jq '.data[0].id'
# → "BUNDLE_ID_RESOURCE_ID"

# 2. App Groups CapabilityをONにする
curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  "https://api.appstoreconnect.apple.com/v1/bundleIdCapabilities" \
  -d '{
    "data": {
      "type": "bundleIdCapabilities",
      "attributes": {
        "capabilityType": "APP_GROUPS"
      },
      "relationships": {
        "bundleId": {
          "data": {
            "type": "bundleIds",
            "id": "BUNDLE_ID_RESOURCE_ID"
          }
        }
      }
    }
  }'

⚠️ 重要な制限: ASC REST APIでできるのはCapabilityの有効/無効切り替えのみ。具体的なApp Groupコンテナ(group.com.example.app)をBundle IDに紐づける操作はAPIでは不可能。settingsパラメータにAPP_GROUP_IDENTIFIERSを指定しても409エラーになる。settingsで対応しているキーはICLOUD_VERSIONDATA_PROTECTION_PERMISSION_LEVELAPPLE_ID_AUTH_APP_CONSENTの3種類のみ。コンテナの紐づけにはfastlane produce associate_group(前セクション参照)またはDeveloper Portal WebUIが必要。

⚡ API経由でCapabilityを変更した場合も、Provisioning Profileの再生成が必要。ただし、コンテナ紐づけが先。Capability ONだけでProfileを再生成してもグループ情報はProfileに含まれない。

Provisioning Profileの再生成

なぜ再生成が必要なのか

Provisioning Profile5には、アプリが使用できるCapability(権限)のリストが埋め込まれている。つまりProfileはBundle IDの「能力証明書」のようなものだ。

Provisioning Profile の中身(概念図):
┌─────────────────────────────────────┐
  Bundle ID: com.example.app          
  Team ID:   ABCDE12345              
  Capabilities:                       
     Push Notifications             
     App Groups                       ここに含まれていないと
     HealthKit                          実機で動かない
  Devices: [iPhone-xxx, Watch-yyy]   
  Expiry: 2027-03-08                 
└─────────────────────────────────────┘

Developer PortalでApp Groups Capabilityを追加しても、既存のProfileにはその変更が反映されない。古いProfileでビルドしたアプリは、実機上でApp Group共有コンテナにアクセスしようとすると失敗する。

再生成手順

Xcode自動管理(推奨):

Xcode → ターゲット → Signing & Capabilities → 「Automatically manage signing」がONなら、次のビルド時にXcodeが自動でProfileを再生成してくれる。

手動管理の場合:

  1. Developer Portal → Profiles → 対象のProfileを選択
  2. 「Edit」→ Capabilitiesが更新されていることを確認
  3. 「Generate」で再生成 → ダウンロード
  4. Xcodeにインストール(ダブルクリック or ドラッグ&ドロップ)

fastlaneの場合:

# Profileを再生成してダウンロード
fastlane sigh --force --app_identifier com.example.app

⚠️ PlayTimeのWatch Smart Stack修正時、Capabilityを追加した後にProfileを再生成し忘れて、実機でWidget Extensionがデータを読めない問題にハマった。Xcode自動管理をONにしていれば防げるが、CI/CDで手動管理している場合は要注意。

Xcodeでの確認方法

Capability追加後、Xcodeでentitlements15ファイルに正しくApp Group識別子が記載されていることを確認する。

<!-- MyApp.entitlements -->
<key>com.apple.security.application-groups</key>
<array>
    <string>group.com.example.app</string>
</array>

このentitlementsファイルはビルド時にProfileとマッチング検証される。Profile側にApp Groupsが含まれていないと、ビルドは通ってもコード署名検証で失敗する。

ASC REST API + fastlaneでProfile再生成を自動化する

ASC REST APIだけでは完結しないが、fastlaneと組み合わせることでDeveloper Portal WebUIもXcode GUIも使わずにProfile再生成まで自動化できる。PlayTimeの開発では、この組み合わせで人の手を煩わせずに解決した。

手順1: App Groups Capabilityを有効化(ASC REST API)

# POST /v1/bundleIdCapabilities でApp Groups ON
curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  "https://api.appstoreconnect.apple.com/v1/bundleIdCapabilities" \
  -d '{ ... }'  # 前セクションのリクエストボディ参照

手順1.5: App Groupコンテナを紐づけ(fastlane — ASC APIでは不可)

# fastlane経由でDeveloper Portal APIを叩く(Apple ID認証が必要)
fastlane produce associate_group \
  -a com.example.app \
  --group-id group.com.example.app

手順2: 古いINVALIDプロファイルを削除

Capabilityを変更すると、既存のProfileは自動的にINVALID状態になる。まずこれを削除する。

# INVALIDなProfileのIDを取得
PROFILE_ID=$(curl -H "Authorization: Bearer $TOKEN" \
  "https://api.appstoreconnect.apple.com/v1/profiles?filter[profileState]=INVALID" \
  | jq -r '.data[] | select(.attributes.name == "YourProfileName") | .id')

# 古いProfileを削除
curl -X DELETE \
  -H "Authorization: Bearer $TOKEN" \
  "https://api.appstoreconnect.apple.com/v1/profiles/$PROFILE_ID"

手順3: 新しいProfileを作成

# 署名用証明書のIDを取得
CERT_ID=$(curl -H "Authorization: Bearer $TOKEN" \
  "https://api.appstoreconnect.apple.com/v1/certificates?filter[certificateType]=IOS_DEVELOPMENT" \
  | jq -r '.data[0].id')

# 新しいProfileを作成
curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  "https://api.appstoreconnect.apple.com/v1/profiles" \
  -d "{
    \"data\": {
      \"type\": \"profiles\",
      \"attributes\": {
        \"name\": \"YourApp Development\",
        \"profileType\": \"IOS_APP_DEVELOPMENT\"
      },
      \"relationships\": {
        \"bundleId\": {
          \"data\": { \"type\": \"bundleIds\", \"id\": \"$BUNDLE_ID_RESOURCE_ID\" }
        },
        \"certificates\": {
          \"data\": [{ \"type\": \"certificates\", \"id\": \"$CERT_ID\" }]
        }
      }
    }
  }"

手順4: レスポンスからProfileをインストール

APIレスポンスのprofileContentフィールドにBase64エンコードされたProfile本体が含まれている。デコードして所定のパスに保存すれば、Xcodeが自動認識する。

# レスポンスからprofileContentを取得してデコード・保存
PROFILE_CONTENT=$(curl -X POST ... | jq -r '.data.attributes.profileContent')

echo "$PROFILE_CONTENT" | base64 --decode \
  > ~/Library/MobileDevice/Provisioning\ Profiles/YourApp_Development.mobileprovision

💡 ASC REST API(Capability ON/OFF + Profile作成/削除)+ fastlane(コンテナ紐づけ)の組み合わせで、Developer Portal WebUIを開かずにProfile再生成まで完結する。profileContent保存後、Xcodeが古いキャッシュを使う場合はDerivedData16のクリアが必要なことがある。

UserDefaults(suiteName:)でのデータ共有

基本パターン

共有コンテナのUserDefaultsにはsuiteNameパラメータでアクセスする。

// 共有UserDefaultsに書き込み(iPhone側)
let sharedDefaults = UserDefaults(suiteName: "group.com.example.app")
sharedDefaults?.set(questList, forKey: "sharedQuests")
sharedDefaults?.synchronize()
// 共有UserDefaultsから読み込み(Widget Extension側)
let sharedDefaults = UserDefaults(suiteName: "group.com.example.app")
let quests = sharedDefaults?.array(forKey: "sharedQuests") as? [[String: Any]]

suiteNameにApp Group識別子を渡すと、通常のUserDefaults.standardとは別の、共有コンテナ上のplist17ファイルが使われる。

💡 synchronize()は通常不要(OSが適切なタイミングで自動同期する)だが、Widget Extensionなど寿命が短いプロセスでは明示的に呼ぶと安全。書き込み直後にプロセスが終了する場合、ディスクへの書き出しが間に合わない可能性があるため。

FileManager.containerURLでのファイル共有

UserDefaultsよりも大きなデータ(画像、JSON、DB等)を共有する場合は、FileManagerで共有コンテナのパスを取得してファイルを読み書きする。

// 共有コンテナのパスを取得
guard let containerURL = FileManager.default.containerURL(
    forSecurityApplicationGroupIdentifier: "group.com.example.app"
) else { return }

// ファイル書き込み(iPhone側)
let dataURL = containerURL.appendingPathComponent("shared_data.json")
try jsonData.write(to: dataURL)

// ファイル読み込み(Widget Extension側)
let dataURL = containerURL.appendingPathComponent("shared_data.json")
let data = try Data(contentsOf: dataURL)

共有コンテナは普通のファイルシステムディレクトリなので、サブディレクトリの作成や任意のファイル形式の保存が可能。

データ共有パターンの比較

方式適用場面データサイズ即時性制約
UserDefaults(suiteName:)設定値・フラグ・小さなデータKB単位高いplist互換型のみ
FileManager.containerURLJSON・画像・大きなデータMB単位中程度ファイルロック未対応18
Core Data19 + 共有コンテナ構造化データ・リレーション制限なし中程度マイグレーション管理が必要
Realm20 + 共有コンテナ構造化データ・リアルタイム通知制限なし高い.realmファイルパスを共有コンテナに指定

Realm DBの共有パターン

Realmを使う場合、DBファイルのパスを共有コンテナ内に指定するだけで、複数プロセスからのアクセスが可能になる。

// Realmの設定(共有コンテナ内にDBファイルを配置)
var config = Realm.Configuration()
if let containerURL = FileManager.default.containerURL(
    forSecurityApplicationGroupIdentifier: "group.com.example.app"
) {
    config.fileURL = containerURL.appendingPathComponent("shared.realm")
}
let realm = try Realm(configuration: config)

RealmはMVCC21(多版型同時実行制御)を採用しているため、複数プロセスからの同時読み書きがロックなしで安全に行える。これはUserDefaultsやFileManagerでの手動ファイル共有よりも堅牢だ。

📝 PlayTimeではRealmを採用しており、iPhone本体アプリとWatch Widget Extensionで同じ.realmファイルを共有コンテナ経由で参照している。Widget ExtensionはRealmから直接クエストデータを読み取ってSmart Stackに表示する。

PlayTimeでの実体験

Watch Smart Stack対応で学んだこと

PlayTimeのWatch Smart Stack修正(cmd_066)で、App Groupの設定を一通り経験した。特に印象に残ったのは以下の点:

  1. App Group識別子のハードコード問題: 当初、App Group識別子をコード内の複数箇所にハードコードしていた。Widget Extension側で識別子を1文字タイプミスして、共有コンテナにアクセスできない問題が発生。定数化して一元管理するようにした。
// ❌ やりがちな間違い: 各所でハードコード
let defaults = UserDefaults(suiteName: "group.com.example.app")  // iPhone側
let defaults = UserDefaults(suiteName: "group.com.exmple.app")   // Ext側(typo!)

// ✅ 正解: 定数化
enum AppConstants {
    static let appGroupID = "group.com.example.app"
}
let defaults = UserDefaults(suiteName: AppConstants.appGroupID)
  1. Provisioning Profile再生成の忘れ: Capabilityを追加した後、Profileの再生成を忘れて実機テストで動かなかった。Xcode自動管理にしておけば安心だが、CI/CDで手動管理している場合はパイプラインにProfile再生成ステップを入れる必要がある。

  2. Widget Extensionの寿命の短さ: Widget Extensionはバックグラウンドで起動され、表示内容を更新したらすぐ終了する。UserDefaultsへの書き込みがsynchronize()なしだとディスクに反映される前にプロセスが死ぬことがある。

学んだこと — ASC APIとProvisioning Profileの落とし穴

今回のApp Group設定を通じて、ドキュメントには書かれていない(あるいは見つけにくい)制限にいくつもぶつかった。

1. ASC REST APIはApp Groupコンテナの紐づけに非対応

ASC REST API13POST /v1/bundleIdCapabilitiesでApp Groups Capabilityの有効/無効切り替えはできる。しかし、具体的なApp Groupコンテナ(group.com.example.app)をBundle IDに紐づける操作はAPIでは不可能だ。

APIドキュメントのcapabilitySettingsで指定できるキーは以下の3種類のみ:

  • ICLOUD_VERSION
  • DATA_PROTECTION_PERMISSION_LEVEL
  • APPLE_ID_AUTH_APP_CONSENT

APP_GROUP_IDENTIFIERSというキーを指定しても無視される。これはAPIの仕様上の制限で、Developer Portal WebUIでは設定できるがAPIでは対応していない。

2. fastlane produce associate_groupが必要

App GroupコンテナのBundle IDへの紐づけには、fastlaneのproduce associate_groupコマンドが必要になる。これはASC REST APIではなく、Developer Portalの内部API10を叩くため、Apple ID + パスワード + 2FA11認証が要求される。

# App GroupコンテナをBundle IDに紐づけ
fastlane produce associate_group \
  -a com.example.app \
  --group-id group.com.example.app

⚡ ASC REST APIのJWTトークン認証とは異なり、produce associate_groupはApple ID認証が必要。CI/CDで使う場合はFASTLANE_USERFASTLANE_PASSWORDの環境変数設定、またはFASTLANE_SESSIONでのセッション管理が必要になる。

3. bundleIdCapabilitiesのsettingsはほぼ非対応

前述の通り、ASC APIのbundleIdCapabilitiesエンドポイントでsettingsパラメータに指定できるキーは3種類だけ。App Groupに限らず、ほとんどのCapabilityの詳細設定はAPIからは操作できない。Capability自体のON/OFFはできるが、細かい設定はDeveloper Portal WebUIかfastlane経由になる。

4. コンテナ未紐づけだとProfileにグループが含まれない

Capabilityを有効化しただけで、App Groupコンテナの紐づけをしていない状態でProvisioning Profileを作成すると、Profileの中身にApp Group情報が含まれない

つまり、以下の順序が重要:

  1. Capability有効化(ASC APIで可能)
  2. コンテナ紐づけ(fastlane or WebUIが必要)
  3. Profile再生成

手順2を飛ばすと、手順3で作ったProfileでビルドしても共有コンテナにアクセスできない。エラーメッセージも分かりにくいため、原因特定に時間がかかる。

5. Automatic signingとmanual signingの競合

Xcodeの「Automatically manage signing」がONの状態で、手動でProfileを~/Library/MobileDevice/Provisioning Profiles/にインストールすると、CODE_SIGN_IDENTITY22が競合することがある。

具体的には、Xcode自動管理がApple Development証明書を選ぶのに対し、手動インストールしたProfileがApple Distribution証明書に紐づいていると、ビルド時に署名エラーが発生する。

⚠️ 自動管理と手動管理を混在させるな。どちらかに統一すること。CI/CDで手動管理する場合は、Xcodeの自動管理をOFFにしてからProfileをインストールする。

なぜfastlaneではApp Groupコンテナ紐づけができるのか

前セクションで「ASC REST APIではApp Groupコンテナの紐づけができない。fastlaneが必要」と書いた。しかし、なぜ同じAppleのシステムに対して、公式APIではできないことがfastlaneではできるのか。この疑問を解くには、Appleが提供する2つの異なるAPIの存在を知る必要がある。

Appleの2つのAPI

ASC REST APIDeveloper Portal内部API
公開状態公式公開(ドキュメントあり)非公開(Webサイト内部通信)
公開時期2018年Apple Developerサイト開設以来
認証方式JWT(API Key + .p8秘密鍵)Apple ID + パスワード + 2FA
エンドポイントapi.appstoreconnect.apple.comdeveloperservices2.apple.com
対応範囲Appleが公式実装した操作のみDeveloper Portalの画面で操作できること全て
安定性公式保証ありAppleが内部変更すれば突然壊れる

ASC REST APIは、Appleが2018年に公式に公開したRESTful API23だ。ドキュメントがあり、認証方式も明確で、安定性が保証されている。ただし、Developer Portalの全機能をカバーしているわけではない。App Groupコンテナの紐づけは、このAPIにまだ実装されていない

一方、Developer Portalの内部APIは、Apple DeveloperのWebサイトがブラウザとサーバー間の通信に使っているHTTPリクエストそのものだ。Webサイトの画面でApp Groupコンテナを紐づけられる以上、その裏側にはHTTPリクエストが飛んでいる。

fastlane spaceshipの仕組み

fastlaneがDeveloper Portalの内部APIを叩ける理由は、spaceship24というライブラリにある。spaceshipはDeveloper PortalのWebサイトが内部で使っているHTTPリクエストをリバースエンジニアリング25して再現するRubyライブラリだ。

ブラウザでの操作:
  ユーザー → Developer Portal WebUI → POST developerservices2.apple.com/... → Appleサーバー

fastlane spaceshipでの操作:
  スクリプト → spaceship (Ruby) → POST developerservices2.apple.com/... → Appleサーバー
                                   ↑ 同じHTTPリクエストを再現

ブラウザでDeveloper Portalにログインしてボタンをクリックする操作と、spaceshipがプログラムから送信するHTTPリクエストは、Appleのサーバーから見れば同じものだ。だから認証方式もブラウザと同じApple ID + パスワード + 2FAが必要になる。

📝 spaceshipという名前は「宇宙船」ではなく、Apple Developer Portalへの「宇宙船のように速い接続」を意味している。Developer Portal WebUIでのポチポチ操作をプログラムで自動化するために生まれたライブラリ。

非公式APIのリスク

spaceshipが叩いているのはAppleが公式に公開したAPIではなく、Webサイトの内部通信を再現したものだ。そのためいくつかのリスクがある:

  • Appleが内部エンドポイントを変更すると壊れる: 実際にAppleがURLやリクエスト形式を変更して、fastlaneのリリースが一時的に壊れたことは何度もある
  • レート制限が不明確: 公式APIにはレート制限のドキュメントがあるが、内部APIには明記されていない
  • Appleの利用規約との関係: 明示的に禁止はされていないが、推奨もされていない

それでも、ASC REST APIがカバーしていない操作(App Groupコンテナ紐づけ、Push Notification証明書の管理等)を自動化するにはspaceshipが事実上唯一の手段だ。

まとめ: 公式APIに機能がなくても自動化は可能

操作                            ASC REST API  spaceship(fastlane)
─────────────────────────────  ────────────  ──────────────────
Capability ON/OFF               ✅            ✅
Profile作成/削除                ✅            ✅
App Groupコンテナ登録           ❌            ✅
App GroupコンテナのBundleID紐づけ ❌           ✅
Push証明書管理                  ❌            ✅

公式APIにインターフェースがなくても、Developer PortalのWebサイト自体にその機能がある限り、Webサイトの裏側のHTTPリクエストをリバースエンジニアリングで再現すれば自動化できる。fastlane spaceshipはまさにこの戦略で、Appleの公式APIの空白を埋めている。

今後の活用方法

新ターゲット追加時の手順書として

PlayTimeのように複数のターゲット(メインアプリ、Watch App、Widget Extension等)を持つプロジェクトでは、新しいターゲットを追加するたびにApp Group設定が必要になる。この記事の手順を「ターゲット追加プロビジョニングチェックリスト」として使える:

  1. Bundle ID登録
  2. App Groups Capability有効化
  3. App Groupコンテナ紐づけ(fastlane produce associate_group
  4. Provisioning Profile作成/再生成
  5. Entitlements確認
  6. コードでの共有コンテナアクセス確認

CI/CD自動化テンプレートとして

ASC REST API + fastlaneの組み合わせで、プロビジョニングの大部分を自動化できる。特にPython + jwt + requestsでASC APIを呼ぶパターンは、シェルスクリプトとしてCI/CDパイプラインに組み込みやすい。Profile再生成のたびにDeveloper Portal WebUIを開く必要がなくなる。

Extension開発チェックリストとして

Widget ExtensionやWatch Extension、Share Extension6を新規に追加する際の「忘れがちなポイント」をまとめたチェックリストとして活用できる:

  • Bundle IDにApp Groups Capability追加済み
  • App Groupコンテナが紐づけ済み
  • Provisioning Profileが再生成済み(コンテナ紐づけ後に)
  • Entitlementsファイルにグループ識別子記載済み
  • コードでsuiteName/containerURLが定数経由で参照されている
  • synchronize()が必要な箇所で呼ばれている

解決に使った具体的な方法

python3 + jwt + requestsでASC REST API呼び出し

JWTトークンの生成からAPI呼び出しまでをPythonで一気通貫に実装した。PyJWT26ライブラリでES256署名を行い、requestsでHTTPリクエストを送信する。

import jwt
import time
import requests

# JWTトークン生成
with open("AuthKey_XXXXXXXXXX.p8", "r") as f:
    private_key = f.read()

payload = {
    "iss": "YOUR_ISSUER_ID",
    "iat": int(time.time()),
    "exp": int(time.time()) + 20 * 60,
    "aud": "appstoreconnect-v1"
}
token = jwt.encode(payload, private_key, algorithm="ES256",
                   headers={"kid": "YOUR_KEY_ID"})

headers = {"Authorization": f"Bearer {token}"}

# Capability有効化
r = requests.post(
    "https://api.appstoreconnect.apple.com/v1/bundleIdCapabilities",
    headers=headers,
    json={
        "data": {
            "type": "bundleIdCapabilities",
            "attributes": {"capabilityType": "APP_GROUPS"},
            "relationships": {
                "bundleId": {
                    "data": {"type": "bundleIds", "id": BUNDLE_ID}
                }
            }
        }
    }
)

curlで複数ステップを手動実行するよりも、スクリプトとしてまとめた方がミスが減る。

fastlane produce associate_groupでコンテナ紐づけ

ASC REST APIではできないApp Groupコンテナの紐づけを、fastlaneで補完する。

# 全ターゲットのBundle IDに同じApp Groupコンテナを紐づけ
for bundle_id in \
  com.example.app \
  com.example.app.watchkitextension \
  com.example.app.widgetextension; do
    fastlane produce associate_group \
      -a "$bundle_id" \
      --group-id group.com.example.app
done

ASC APIでProfile再生成 + base64デコード + ローカルインストール

コンテナ紐づけが完了した後、ASC APIでProfileを再生成し、レスポンスのBase64エンコード済みProfileをデコードしてローカルにインストールする。この手順はXcode GUIもDeveloper Portal WebUIも不要で、ターミナルだけで完結する。

# 1. 古いINVALIDプロファイルを削除
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
  "https://api.appstoreconnect.apple.com/v1/profiles/$OLD_PROFILE_ID"

# 2. 新プロファイル作成(レスポンスにBase64のprofileContent含む)
RESPONSE=$(curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  "https://api.appstoreconnect.apple.com/v1/profiles" \
  -d '{ ... }')

# 3. Base64デコードしてインストール
echo "$RESPONSE" | jq -r '.data.attributes.profileContent' \
  | base64 --decode \
  > ~/Library/MobileDevice/Provisioning\ Profiles/MyApp_Dev.mobileprovision

📝 この方法で、殿(人間)の手を一切煩わせずにCapability追加からProfile再生成・インストールまでを自動解決できた。Developer Portal WebUIのGUIクリック作業が丸ごと不要になるので、特にCI/CD環境では大きな時間節約になる。

まとめ

ステップやること
1. App Group識別子の登録Developer Portal or fastlane produce group
2. Capabilityの有効化Developer Portal GUI / fastlane / ASC REST API
3. Provisioning Profileの再生成Xcode自動 or fastlane sigh --force
4. Entitlementsの確認.entitlementsファイルにApp Group IDが記載
5. コードでデータ共有UserDefaults(suiteName:) / FileManager.containerURL / Realm

App Groupの設定自体はシンプルだが、Provisioning Profileの再生成を忘れたり、識別子をハードコードしてタイプミスしたりと、地味なハマりポイントが多い。特にCI/CD環境ではProfileの管理を自動化しておくことが重要だ。

Footnotes

  1. Smart Stack — Apple WatchのウィジェットスタックUI。複数のウィジェットを重ねて表示し、時間帯やコンテキストに応じて最適なウィジェットを自動的に前面に出す機能。

  2. Widget Extension — ホーム画面やSmart Stackにウィジェットを表示するためのApp Extension。メインアプリとは別プロセスで動作する。

  3. サンドボックス — アプリごとに隔離されたファイルシステム・メモリ空間。他のアプリやOSの領域にアクセスできないようにするセキュリティ機構。

  4. App Group — 同じ開発者の複数のアプリ(またはApp Extension)間でデータを共有するためのiOSの仕組み。共有コンテナを提供する。

  5. Provisioning Profile — アプリの署名・実行権限・使用可能Capabilityを証明するファイル。開発用・配布用がある。Bundle ID、Team ID、デバイスリスト、Capabilitiesが埋め込まれている。 2

  6. Share Extension — 他のアプリからの「共有」アクションを受け取るApp Extension。Safari等からURLやテキストを受け取って処理する。 2

  7. Apple Developer Portal — Apple Developer Programメンバーが証明書、Bundle ID、Provisioning Profile等を管理するWebサイト(developer.apple.com)。

  8. Bundle ID — アプリを一意に識別する文字列(例: com.example.app)。逆ドメイン形式で記述する。

  9. fastlane — iOSアプリのビルド・テスト・リリースを自動化するオープンソースツール群。証明書管理、スクリーンショット撮影、App Store申請等を自動化できる。

  10. Apple Developer Portalの内部API。ASC REST APIとは別のエンドポイントで、Apple ID認証が必要。fastlaneはこのAPIをリバースエンジニアリングして利用している。 2

  11. 2FA — Two-Factor Authentication(二要素認証)。パスワードに加えて、信頼済みデバイスに送信される6桁コードで本人確認する仕組み。 2

  12. CI/CD — Continuous Integration / Continuous Delivery(継続的インテグレーション / 継続的デリバリー)。コードの変更を自動でビルド・テスト・デプロイする仕組み。

  13. App Store Connect API — App Store ConnectをプログラマティックにOperateするためのREST API。Bundle ID管理、Capability設定、ビルド管理等が可能。 2

  14. JWT — JSON Web Token。ヘッダー・ペイロード・署名の3パートで構成されるトークン形式。App Store Connect APIではES256アルゴリズムで署名する。

  15. Entitlements — アプリが要求する権限(App Groups、Push Notifications、HealthKit等)を宣言するplistファイル。ビルド時にProvisioning Profileと照合される。

  16. DerivedData — Xcodeがビルド成果物やインデックスをキャッシュするディレクトリ(~/Library/Developer/Xcode/DerivedData/)。Provisioning Profileを手動更新した際、古いキャッシュが残ると署名エラーが出ることがある。

  17. plist互換型 — Property List形式で表現可能なデータ型。String, Int, Double, Bool, Data, Date, Array, Dictionaryに限定される。カスタムオブジェクトは直接保存できない。

  18. 複数プロセスから同一ファイルに同時書き込みすると競合が起きる可能性がある。NSFileCoordinatorを使えば協調できるが、実装が煩雑。

  19. Core Data — Appleが提供するオブジェクトグラフ管理・永続化フレームワーク。SQLiteをバックエンドとし、マイグレーション機能を備える。

  20. Realm — モバイル向けのオブジェクトデータベース。Core Dataより簡潔なAPIを持ち、リアルタイム通知やマルチプロセスアクセスに対応。

  21. MVCC — Multi-Version Concurrency Control(多版型同時実行制御)。読み取りと書き込みが互いにブロックしないデータベース並行制御方式。

  22. CODE_SIGN_IDENTITY — Xcodeビルド設定の一つ。コード署名に使用する証明書を指定する。Apple Development(開発用)とApple Distribution(配布用)がある。

  23. RESTful API — REST(Representational State Transfer)アーキテクチャに従ったWeb API。HTTPメソッド(GET/POST/PUT/DELETE)でリソースを操作する設計様式。

  24. spaceship — fastlaneの内部ライブラリ。Apple Developer PortalやApp Store ConnectのWebサイトが使う内部HTTPリクエストをリバースエンジニアリングで再現し、プログラムから操作可能にする。

  25. リバースエンジニアリング — ソフトウェアやシステムの動作を解析して、内部の仕組みやプロトコルを明らかにする技術。ここではブラウザの開発者ツールでHTTPリクエストを観察し、同じリクエストをプログラムから再現する手法を指す。

  26. PyJWT — PythonでJWTトークンの生成・検証を行うライブラリ。pip install PyJWT[crypto]でES256署名に対応。