本記事は生成AIと共同で執筆しています。事実関係は可能な範囲で公式ドキュメント等と照合していますが、誤りが含まれている可能性があります。重要な判断を行う前にご自身でも一次情報をご確認ください。

前作 フォルダの画像群から TEI/XML を生成する macOS アプリの開発 では、版面画像を OCR して TEI(Text Encoding Initiative)/XML を生成する macOS アプリ TEI Scanner を、Developer ID 署名 + Notarization で .dmg 配布するところまでを扱いました。本記事はその続きで、同じアプリを Mac App Store にも提出し、審査を通過するまでの記録です。

App Store Connect API を使った提出の基本フローは、iOS 向けに App Store Connect API だけで iOS アプリを審査提出する手順 でまとめています。本記事は重複を避け、macOS で iOS と異なる点を中心に書きます。スクリーンショット生成の自動化は App Store のスクリーンショット生成を Python + UI テストで完全自動化する が、リジェクト後の再提出は App Store 審査リジェクト後の修正・再提出を App Store Connect API で実行する が参考になります。

提出から審査結果までは、2026年5月10日に提出し、4日後に審査通過(配信可能)の通知が届きました。

対象アプリの画面は次の通りです(実装の詳細は前作を参照)。

起動直後TEI/XML 表示
空状態TEI/XML 表示

Developer ID 配布と Mac App Store の違い

同じ .app を配布するにしても、Developer ID 経由(前作)と Mac App Store 経由では工程がいくつか分かれます。

項目Developer ID 配布Mac App Store
App Sandbox任意必須
配布形式.dmg(または .pkg.pkg(App Store Connect にアップロード)
署名証明書Developer ID ApplicationApple Distribution + インストーラ署名証明書
Notarizationnotarytool で明示的に実施審査時に Apple 側で自動的に処理
配布チャネルGitHub Releases など任意App Store のみ
審査なしあり

前作で組んだアーカイブ用スクリプト(xcodegen generatexcodebuild archive)はそのまま流用し、エクスポート以降を App Store 用に分岐させる構成にしました。

scripts/archive.sh --devid      # Developer ID + notarized .dmg(前作)
scripts/archive.sh --appstore   # Mac App Store .pkg + アップロード(本記事)

App Sandbox を有効にする

Mac App Store に出すアプリは App Sandbox が必須です。Developer ID 配布では任意なので、前作の時点では entitlements を最小限にしていましたが、App Store 提出にあたって以下を設定しました。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
	<key>com.apple.security.files.user-selected.read-write</key>
	<true/>
</dict>
</plist>

App Sandbox を有効にすると、アプリは利用者が明示的に選んでいないパスにはアクセスできなくなります。TEI Scanner はフォルダ選択ダイアログ(NSOpenPanel)とウィンドウへのドラッグ&ドロップ経由でしかフォルダを開かない設計だったため、com.apple.security.files.user-selected.read-write の 1 つで足りました。同梱しているサンプル画像はアプリバンドル内にあるので、追加の entitlement なしで読めます。OCR は Apple の Vision フレームワークで端末内処理し、ネットワーク通信もしないため、com.apple.security.network.client のような entitlement も不要でした。

サンドボックス設計が単純で済んだのは、前作の段階で「フォルダ選択かドラッグ&ドロップでしかフォルダを開かない」という入口に絞っていたことが結果的に効いたかたちです。

ビルドのエクスポート: .pkg と altool

App Store 用のエクスポートは、.dmg ではなく .pkg を作ります。xcodebuild -exportArchive に渡す exportOptionsmethodapp-store-connect にするのが分岐点です。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>method</key>
	<string>app-store-connect</string>
	<key>teamID</key>
	<string>(Team ID)</string>
	<key>signingStyle</key>
	<string>automatic</string>
	<key>uploadSymbols</key>
	<true/>
	<key>manageAppVersionAndBuildNumber</key>
	<false/>
	<key>destination</key>
	<string>export</string>
</dict>
</plist>

signingStyleautomatic にし、xcodebuild-allowProvisioningUpdates を渡すことで、App Store 提出に必要な署名証明書(アプリ署名用とインストーラ署名用)がクラウド管理で用意されました。前作で Developer ID Application 証明書が自動作成されたのと同じ挙動です。manageAppVersionAndBuildNumberfalse にして、バージョン番号とビルド番号は project.yml 側で管理する方針にしています。

アップロードは、Notarization で使った notarytool ではなく altool を使います。

xcrun altool --upload-app \
  --type macos \
  --file "$PKG_FILE" \
  --apiKey "$APP_STORE_API_KEY" \
  --apiIssuer "$APP_STORE_API_ISSUER"

--type が iOS(ios)との差分で、macOS は macos を指定します。--apiKey / --apiIssuer で App Store Connect API キーをそのまま渡せるため、前作の .env に置いた資格情報を使い回せました。App Store 提出版の Notarization は審査の過程で Apple 側が処理するので、notarytool を明示的に呼ぶ必要はありません。アップロード後、ビルドが App Store Connect 側で処理を終えて VALID になるまでは、数分から30分程度かかることが多いようです。

なお altool は現在 legacy 扱いの位置づけで、Apple は Transporter アプリ、xcodebuild -exportArchive のアップロード機能、App Store Connect API への移行を案内しています。本構成では前作の .env をそのまま渡せる手軽さから altool を採用していますが、新規に組む場合はこれらの経路も検討に値します。

メタデータ投入: filter[platform]=MAC_OS

ここから先は App Store Connect API を叩く Python スクリプト群です。基本構造は前掲の iOS 記事と同じで、JWT(JSON Web Token)を組んで https://api.appstoreconnect.apple.com/v1/ に投げます。

iOS フローとの最も分かりやすい API 差分は、アプリのバージョンを引くクエリにプラットフォームのフィルタを付ける点です。

def get_editable_version_id(app_id):
    _, data = request("GET", f"apps/{app_id}/appStoreVersions?filter[platform]=MAC_OS")
    versions = data.get("data", [])
    editable_states = {"PREPARE_FOR_SUBMISSION", "DEVELOPER_REJECTED",
                       "REJECTED", "METADATA_REJECTED"}
    for v in versions:
        if v["attributes"]["appStoreState"] in editable_states:
            return v["id"]
    raise SystemExit("No editable version found.")

filter[platform]=MAC_OS を付けないと、同一アプリレコードに iOS 版バージョンが併存している場合に取り違える可能性があります。1 プラットフォームしかなくても、明示しておくほうが安全でした。

メタデータ投入スクリプトでは、次の項目を冪等(何度実行しても同じ結果)に設定しています。

  • appStoreVersionLocalizations(ja / en-US): 説明文、キーワード、プロモーションテキスト、サポート URL
  • appInfos: 第一カテゴリ(PRODUCTIVITY)と第二カテゴリ(REFERENCE
  • appStoreVersions: 著作権表記
  • apps: コンテンツ権利の申告(DOES_NOT_USE_THIRD_PARTY_CONTENT
  • appInfoLocalizations: プライバシーポリシー URL
  • ageRatingDeclarations: 年齢レーティング(全項目 NONE / 真偽値は全 false、結果は 4+)

カテゴリは appCategories リソースの ID(PRODUCTIVITY などの文字列)をリレーションシップに直接指定します。年齢レーティングは項目数が多いので、辞書を 1 つ定義してまとめて PATCH しています。

スクリーンショット: APP_DESKTOP / 2880×1800

スクリーンショットは macOS と iOS で扱いが分かれます。iOS は端末サイズごとに screenshotDisplayType が複数(APP_IPHONE_67APP_IPAD_PRO_129 など)に分かれますが、macOS は APP_DESKTOP の 1 種類だけです。受け付けられる解像度は 1280×800 / 1440×900 / 2560×1600 / 2880×1800 のいずれかで、本アプリでは最大の 2880×1800 を採用しました。

撮影自体は、前作でアプリに組み込んだ自己スクリーンショット機能をそのまま使い、出力された PNG を sips で 2880×1800 まで白パディングします。

PAD_W=2880
PAD_H=1800
for f in docs/screenshots/0*.png; do
  out="docs/asc-screenshots/$(basename "$f")"
  cp "$f" "$out"
  sips --padToHeightWidth "$PAD_H" "$PAD_W" --padColor FFFFFF "$out" --out "$out"
done

sips --padToHeightWidth は元画像を中央に置いて指定サイズまで余白を足します。アプリのウィンドウは正方形に近い比率ではないため、撮影解像度をそのまま使うと App Store Connect が受け付ける比率に合わないことがあり、パディングで規定サイズに揃えるのが扱いやすい方法でした。パディング後の画像は次のように、左右に白余白が入って 2880×1800 に揃います。

パディング後(OCR 完了画面)パディング後(TEI/XML 表示)
App Store 用スクリーンショット 1App Store 用スクリーンショット 2

アップロードは appScreenshotSetsdisplayTypeAPP_DESKTOP)に対して行います。1 枚ごとに「予約 → バイナリを PUT → チェックサムを添えて commit」の 3 段階を踏む点は iOS と共通です。

ビルド添付と暗号化コンプライアンス

アップロードしたビルドが VALID になったら、編集中のバージョンに紐付けます。あわせて、暗号化コンプライアンスの申告も API から行えます。

def declare_encryption(build_id):
    request("PATCH", f"builds/{build_id}", {
        "data": {
            "type": "builds",
            "id": build_id,
            "attributes": {"usesNonExemptEncryption": False},
        }
    })

TEI Scanner は OCR をローカルで完結させ、HTTPS 通信も行わないため、輸出規制の対象となる暗号化は使用していません。usesNonExemptEncryptionfalse にすることで、毎回のアップロードごとに Web UI で同じ申告をする手間を省けます。

レビュー詳細と審査提出

審査担当者向けの情報(appStoreReviewDetails)も API から設定できます。TEI Scanner はログイン不要なのでデモアカウントは「不要」とし、notes に再現手順を書きました。

REVIEW_DETAILS = {
    "contactFirstName": "Satoru",
    "contactLastName": "Nakamura",
    "contactEmail": "(連絡先メール)",
    "contactPhone": "(連絡先電話番号)",
    "demoAccountRequired": False,
    "notes": (
        "TEI Scanner runs entirely on-device. No login or network access "
        "is required. To exercise the app: launch, click 'Try sample' on "
        "the empty window to load two bundled English page images, click "
        "'Run OCR', then 'Export TEI/XML…' and save the result anywhere."
    ),
}

審査担当者がネットワークやアカウントなしで一通り動作を確認できるよう、「空画面で『サンプルを試す』→『OCR を実行』→『TEI/XML を書き出し』」という最短手順を明記しています。前作で同梱サンプルを用意しておいたことが、ここで審査担当者の動線としても役立ちました。

審査提出は reviewSubmissions を作り、reviewSubmissionItems でバージョンを紐付け、submittedtruePATCH する 3 ステップです。reviewSubmissionsplatformMAC_OS を指定する以外は、iOS の流れと同じ構造でした。

request("POST", "reviewSubmissions", {
    "data": {
        "type": "reviewSubmissions",
        "attributes": {"platform": "MAC_OS"},
        "relationships": {
            "app": {"data": {"type": "apps", "id": app_id}}
        },
    }
})

App Privacy だけは Web UI

App Privacy(プライバシーに関するデータ収集の申告)は、調査した限り API が用意されておらず、App Store Connect の Web UI から設定する必要がありました。TEI Scanner は端末内ですべての処理が完結し、解析・テレメトリ・広告 ID も持たないため「データを収集しません(Data Not Collected)」を選択しています。この申告を済ませないと審査に提出できないため、提出スクリプトを走らせる前の段階で Web UI 側を片付けておく形になりました。

この点は iOS 提出時と同じで、API だけで完結しない数少ない工程です。

審査結果と公開

2026年5月10日に提出し、4日後に「審査が完了し、配信可能(eligible for distribution)になった」旨の通知が届きました。1.0 は追加のやり取りなく通過しています。

注意したい点として、「審査通過」と「App Store での公開」は別の段階です。バージョンの公開方式(releaseType)が自動なら審査通過後にそのまま公開され、手動なら開発者が明示的にリリース操作をするまで公開されません。審査通過の通知が届いても App Store のページがすぐ表示されないことがあり、Apple のアナウンスでも公開後にカタログへ反映されるまで最大24時間程度かかるとされています。公開状態はバージョンの状態(手動リリース待ちか、配信可能か)で確認できます。

なお「審査通過」と「公開」のあいだには、本記事では触れていないもう1つの軸 — 配信地域(App Availability)— がありました。本アプリは提出フローを API 中心で組んだ結果、配信地域が一度も設定されないまま審査を通過してしまい、通過後も App Store に並びませんでした。その切り分けと、App Store Connect API での後追い対処は 審査は通ったのに App Store に出ない — 配信地域(App Availability)を App Store Connect API で後追い設定する に分けてまとめています。

iOS 提出フローとの差分まとめ

最後に、本アプリで iOS との差分として現れた箇所を一覧にします。

工程iOSmacOS
アーカイブの destinationgeneric/platform=iOSgeneric/platform=macOS
App Sandbox常にサンドボックス内entitlement を明示的に追加
配布形式.ipa.pkg
アップロードaltool --type iosaltool --type macos
バージョン取得フィルタfilter[platform]=IOSfilter[platform]=MAC_OS
スクリーンショットの displayTypeAPP_IPHONE_67 ほか複数APP_DESKTOP のみ
スクリーンショット解像度端末サイズごと2880×1800 など 4 種から選択
reviewSubmissionsplatformIOSMAC_OS

App Store Connect API の骨格は iOS と macOS でほぼ共通で、差分はプラットフォーム識別子の指定とスクリーンショットの表示タイプに集約されていました。前作で .env ベースの CLI 化と自己スクリーンショット機能を用意していたため、App Store 向けの追加分はスクリプト数本で収まっています。

参考