ターゲットを本当の「MAS 用」にする

1 編では、MAS 専用の Bundle ID を登録し、FocusTimer MAS ビルドターゲットを複製しました。しかし、そのターゲットはまだ直接配布ターゲットのコピーに過ぎません。

MAS ビルドは直接配布ビルドと 3 つの点で異なる必要があります。

  1. 権限 (entitlements) — App Store に適した最小限の権限のみ
  2. Info.plist — Sparkle のキーを除外し、App Store のメタデータを追加
  3. コード — Sparkle がなくてもコンパイルできるよう分岐

この記事ではこの 3 つをすべて分けます。

1 編と同様、FocusTimercom.example.FocusTimer.mas などはすべてサンプル値です。

ステップ 1 — MAS 専用の entitlements ファイル

entitlements はアプリがどのシステム権限を要求するかを記述したファイルです。直接配布ビルドでは Sparkle がアップデートのインストールのために追加の権限を必要としていましたが、MAS ビルドには Sparkle がないため、それらの権限は不要です。

プロジェクトルートに FocusTimer-MAS.entitlements ファイルを新規作成します。

<?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>

直接配布用の FocusTimer.entitlements と比べると、次のように異なります。

  • Sparkle 関連の権限なし — 直接配布ビルドにあった temporary-exception.mach-lookup.* (Sparkle がインストールヘルパーと通信するための例外) は、MAS ビルドに置く理由がありません。
  • network.client なし — Sparkle が外れると、FocusTimer 本体はどんなネットワーク呼び出しも行いません。使わない権限を外すのが正直であり、審査でも不必要な権限は指摘の対象となります。アプリが要求する権限の表面は小さければ小さいほど良いです。

上記の例は「ユーザーが自ら選んだファイルを読み書きする」程度の権限しか持たない最もシンプルな形です。ご自身のアプリが実際にネットワークや他のリソースを使う場合は対応する権限を追加しますが、使わない権限は絶対に追加しないでください。

ステップ 2 — MAS 専用の Info.plist ファイル

同様に、プロジェクトルートに FocusTimer-MAS-Info.plist を新規作成します。

<?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>LSApplicationCategoryType</key>
    <string>public.app-category.productivity</string>
    <key>ITSAppUsesNonExemptEncryption</key>
    <false/>
</dict>
</plist>

直接配布用の FocusTimer-Info.plist との違いは 2 つあります。

① Sparkle の SU* キーをすべて除外する

直接配布用の Info.plist には SUFeedURLSUPublicEDKey などの Sparkle 設定キーが含まれていました。MAS 用の Info.plist にはこれらのキーが一つも含まれていてはいけません。App Store の審査は自己完結型の自動アップデートを禁止しているため、そのような機能を示唆するキーが plist に残っているだけで問題になり得ます。キー自体を置かないのが安全です。

② App Store のメタデータキーを追加する

  • LSApplicationCategoryType — App Store でアプリが属するカテゴリーです。上記の例の public.app-category.productivity は「生産性」カテゴリーを意味します。この値は 3 編で App Store Connect に設定するカテゴリーと一致させなければ、審査の段階で不一致の警告が表示されます。
  • ITSAppUsesNonExemptEncryption — アプリが輸出規制対象の暗号化技術を使用しているかどうかです。一般的な暗号化 (例: HTTPS、標準システム API) のみを使用するか、暗号化を使用しない場合は false に設定できます。このキーをあらかじめ設定しておくと、ビルドをアップロードするたびに表示される暗号化に関するアンケートを自動的にパスできます。

ITSAppUsesNonExemptEncryptionfalse に設定する前に、ご自身のアプリが本当に標準外の暗号化を使用していないか確認してください。判断できない場合は、Apple の暗号化関連ドキュメントを参照するのが安全です。

ステップ 3 — MAS ターゲットのビルド設定

次は、1 編の 2-3 節で先送りにしたビルド設定の更新です。TARGETS → FocusTimer MAS → Build Settings で次の項目を設定します。

ビルド設定キー
PRODUCT_BUNDLE_IDENTIFIERcom.example.FocusTimer.mas
INFOPLIST_FILEFocusTimer-MAS-Info.plist
CODE_SIGN_ENTITLEMENTSFocusTimer-MAS.entitlements
ENABLE_APP_SANDBOXYES
ENABLE_HARDENED_RUNTIMEYES
ENABLE_USER_SELECTED_FILESreadwrite
CODE_SIGN_STYLEAutomatic
MARKETING_VERSIONCURRENT_PROJECT_VERSION直接配布ターゲットと同じ値

ポイントをまとめると次のとおりです。

  • INFOPLIST_FILECODE_SIGN_ENTITLEMENTS が先ほど作成した MAS 専用ファイルを指していなければなりません。この 2 行を直接配布用のファイルのままにしておくミスが多くあります。
  • CODE_SIGN_STYLEAutomatic にすると、Xcode が Mac App Store 用のプロビジョニングプロファイルと Apple Distribution 証明書を自動的に発行・マッチングしてくれます。手動で署名を管理する必要はありません。
  • ネットワークを使わないアプリの場合は ENABLE_OUTGOING_NETWORK_CONNECTIONS は設定しません。entitlements から network.client を外したのと同じ考え方です。
  • バージョン番号 (MARKETING_VERSION など) は直接配布ターゲットと同じ値に保ち、2 つのチャネルのバージョンがずれないようにします。

ステップ 4 — Sparkle コードの分岐 (#if canImport(Sparkle))

設定ファイルを分けても、一つの問題が残ります。ソースコードのどこかに import Sparkle と Sparkle を使うコードがありますが、MAS ターゲットには Sparkle がリンクされないため、その import 文自体がコンパイルエラーを引き起こします。

解決策は、Swift の条件付きコンパイル指示文 #if canImport(Sparkle) で Sparkle 関連のコードを囲むことです。

なぜ canImport なのか — 別のフラグを使わない理由

#if MAS_BUILD のような独自のコンパイルフラグで分岐することもできます。しかし、そうすると「MAS ターゲットにはそのフラグをオン、直接配布ターゲットにはオフ」という設定を手動で同期し続けなければなりません。ターゲットが増えたり設定が変わったりすると、ずれが生じやすくなります。

canImport(Sparkle) は違います。これは**「Sparkle.framework がこのターゲットにリンクされているか」をコンパイラが直接確認します。1 編で MAS ターゲットのパッケージ依存関係から Sparkle を削除したため、MAS ターゲットでは canImport(Sparkle)自動的に false** になります。別途同期するフラグはありません。そのため、macOS アプリが MAS チャネルを分離する際、このパターンを標準的に使用します。

分岐① — Sparkle 専用ファイルは全体を囲む

自動アップデートロジックだけを含むファイル (例: UpdaterCoordinator.swift) は、ファイル全体を #if で囲みます。

#if canImport(Sparkle)
import Foundation
import Sparkle
import Combine

@Observable
@MainActor
final class UpdaterCoordinator {
    // Sparkle アップデーターをラップするコード …
}
#endif

MAS ビルドでは、このファイルは空のファイルのようにコンパイルされ、何の影響も与えません。

分岐② — Sparkle を使う側も一緒に囲む

UpdaterCoordinator を作成して画面に渡すコードも一緒に分岐させる必要があります。そうしなければ、MAS ビルドで「存在しない型」を参照することになります。

@main
struct FocusTimerApp: App {
    @State private var viewModel = CompositionRoot.makeContentViewModel()

    #if canImport(Sparkle)
    @State private var updater = UpdaterCoordinator()
    #endif

    var body: some Scene {
        Settings {
            #if canImport(Sparkle)
            PreferenceView(viewModel: viewModel, updater: updater)
            #else
            PreferenceView(viewModel: viewModel)
            #endif
        }

        MenuBarExtra {
            #if canImport(Sparkle)
            MenuBarContent(updater: updater)
            #else
            MenuBarContent()
            #endif
        } label: {
            // メニューバーアイコン …
        }
    }
}

private struct MenuBarContent: View {
    #if canImport(Sparkle)
    @Bindable var updater: UpdaterCoordinator
    #endif

    var body: some View {
        Button("FocusTimer を表示") { /* … */ }

        #if canImport(Sparkle)
        Button("アップデートを確認…") { updater.checkForUpdates() }
            .disabled(!updater.canCheckForUpdates)
        #endif

        // その他の共通メニュー …
    }
}

ポイントは 3 つです。

  • updaterフィールド宣言自体を #if で囲みます。
  • updater受け取る画面 (PreferenceViewMenuBarContent) は、Sparkle がある場合とない場合の 2 つの形に分岐します。
  • 「アップデートを確認…」のような Sparkle 専用の UI 要素#if で囲みます。MAS ビルドではこのメニューがまったく表示されません — App Store 版には自己アップデートメニューがないのが正しい姿です。

設定画面 (PreferenceView) にある「自動アップデートを有効にする」トグルなどの要素もすべて同じパターンで囲めば大丈夫です。

2 編まとめ

ここまで進んだ方は、以下が揃っているはずです。

  • ✅ MAS 専用の権限ファイル FocusTimer-MAS.entitlements (最小権限)
  • ✅ MAS 専用の FocusTimer-MAS-Info.plist (Sparkle キーを削除、App Store メタデータを追加)
  • ✅ MAS ターゲットのビルド設定の整理
  • #if canImport(Sparkle) で分岐された自動アップデートコード

これで FocusTimer MAS ターゲットは直接配布ターゲットとは本当に異なる、App Store に出せる形になりました。残る作業は、このビルドを実際に App Store Connect にアップロードする道を整えることです。

次の編では、アップロード用の ExportOptions-MAS.plist を作成し、App Store Connect にアプリレコードを登録し、2 つのチャネルが今後も壊れないよう検証する方法でシリーズを締めくくります。