Rendre la cible vraiment « spécifique à MAS »

En Partie 1, nous avons enregistré un Bundle ID exclusif MAS et dupliqué la cible de build FocusTimer MAS. Mais cette cible n’est encore qu’une copie de la cible de distribution directe.

Un build MAS doit différer du build de distribution directe sur trois points.

  1. Entitlements (droits d’accès) — uniquement l’ensemble minimum approprié pour l’App Store
  2. Info.plist — supprimer les clés Sparkle, ajouter les métadonnées App Store
  3. Code — brancher pour qu’il compile même sans Sparkle

Dans cet article, nous séparerons les trois.

Comme en Partie 1, FocusTimer, com.example.FocusTimer.mas, etc. sont tous des valeurs d’exemple.

Étape 1 — Le fichier entitlements exclusif MAS

Les entitlements constituent un fichier qui liste les permissions système qu’une application demande. Dans le build de distribution directe, Sparkle avait besoin de permissions supplémentaires pour installer des mises à jour, mais le build MAS n’a pas Sparkle, donc ces permissions sont inutiles.

Créez un nouveau fichier FocusTimer-MAS.entitlements à la racine du projet.

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

Comparé au FocusTimer.entitlements de distribution directe, voici comment il diffère.

  • Pas de permissions liées à Sparkle — Les entrées temporary-exception.mach-lookup.* du build de distribution directe (l’exception qui permet à Sparkle de communiquer avec le helper d’installation) n’ont aucune raison d’être dans le build MAS.
  • Pas de network.client — Avec Sparkle supprimé, l’application FocusTimer elle-même n’effectue aucun appel réseau. Il est honnête de supprimer les permissions que vous n’utilisez pas, et les permissions inutiles sont également signalées lors de la validation. Plus la surface de permissions qu’une application demande est petite, mieux c’est.

L’exemple ci-dessus est la forme la plus simple possible, avec juste assez de permission pour « lire et écrire des fichiers que l’utilisateur sélectionne explicitement ». Si votre application utilise réellement le réseau ou d’autres ressources, ajoutez les permissions correspondantes, mais n’incluez jamais des permissions que vous n’utilisez pas.

Étape 2 — Le fichier Info.plist exclusif MAS

De même, créez un nouveau FocusTimer-MAS-Info.plist à la racine du projet.

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

Il y a deux différences par rapport au FocusTimer-Info.plist de distribution directe.

① Toutes les clés Sparkle SU* sont supprimées

Le Info.plist de distribution directe contenait des clés de configuration Sparkle comme SUFeedURL et SUPublicEDKey. Le Info.plist MAS ne doit contenir aucune de ces clés. La validation de l’App Store interdit les mises à jour automatiques intégrées, donc même une clé qui suggère simplement une telle fonctionnalité restant dans le plist peut poser problème. Il est plus sûr de ne pas inclure la clé du tout.

② Les clés de métadonnées App Store sont ajoutées

  • LSApplicationCategoryType — la catégorie à laquelle appartient l’application sur l’App Store. Le public.app-category.productivity dans l’exemple ci-dessus signifie la catégorie « Productivité ». Cette valeur doit correspondre à la catégorie que vous définissez dans App Store Connect en Partie 3, pour qu’un avertissement de non-concordance n’apparaisse pas lors de la validation.
  • ITSAppUsesNonExemptEncryption — si l’application utilise une technologie de chiffrement soumise à des réglementations d’exportation. Si vous utilisez uniquement du chiffrement standard (par ex., HTTPS, API système standard) ou pas de chiffrement du tout, vous pouvez mettre false. Coder cette clé en dur à l’avance permet de passer automatiquement le questionnaire sur le chiffrement qui apparaît à chaque fois que vous téléversez un build.

Avant de mettre ITSAppUsesNonExemptEncryption à false, vérifiez que votre application n’utilise vraiment pas de chiffrement non standard. Si vous n’êtes pas sûr, il est plus sûr de consulter la documentation d’Apple sur le chiffrement.

Étape 3 — Paramètres de build de la cible MAS

Voici maintenant la mise à jour des paramètres de build que nous avons reportée en Partie 1, Section 2-3. Dans TARGETS → FocusTimer MAS → Build Settings, alignez les points suivants.

Clé de paramètre de buildValeur
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_VERSION, CURRENT_PROJECT_VERSIONIdentiques à la cible de distribution directe

Les points clés sont les suivants.

  • INFOPLIST_FILE et CODE_SIGN_ENTITLEMENTS doivent pointer vers les fichiers exclusifs MAS que vous venez de créer. Une erreur courante consiste à laisser ces deux lignes pointant vers les fichiers de distribution directe.
  • Définir CODE_SIGN_STYLE sur Automatic permet à Xcode d’émettre et de faire correspondre automatiquement le profil de provisionnement Mac App Store et le certificat Apple Distribution. Vous n’avez pas besoin de gérer la signature vous-même.
  • Si votre application n’utilise pas le réseau, ne définissez pas ENABLE_OUTGOING_NETWORK_CONNECTIONS. C’est le même raisonnement que la suppression de network.client des entitlements.
  • Gardez les numéros de version (MARKETING_VERSION, etc.) aux mêmes valeurs que la cible de distribution directe, pour que les versions des deux canaux ne divergent pas.

Étape 4 — Brancher le code Sparkle (#if canImport(Sparkle))

Même après avoir séparé les fichiers de configuration, un problème demeure. Quelque part dans le code source, il y a import Sparkle et du code qui utilise Sparkle, mais Sparkle n’est pas lié dans la cible MAS, donc l’instruction import elle-même produit une erreur de compilation.

La solution consiste à envelopper le code lié à Sparkle avec la directive de compilation conditionnelle Swift #if canImport(Sparkle).

Pourquoi canImport — Pourquoi ne pas utiliser un flag séparé

Vous pourriez également brancher avec un flag de compilation personnalisé comme #if MAS_BUILD. Mais vous devriez alors maintenir manuellement le réglage « activer le flag pour la cible MAS, désactiver pour la cible de distribution directe ». C’est facile à désynchroniser à mesure que les cibles se multiplient ou que les paramètres changent.

canImport(Sparkle) est différent. Il fait vérifier directement par le compilateur si « Sparkle.framework est lié dans cette cible ». Puisque nous avons supprimé Sparkle des dépendances de packages de la cible MAS en Partie 1, canImport(Sparkle) est automatiquement faux dans la cible MAS. Il n’y a pas de flag séparé à maintenir synchronisé. C’est pourquoi les applications macOS utilisent ce modèle comme norme de facto lors de la séparation d’un canal MAS.

Branchement ① — Envelopper entièrement les fichiers exclusifs Sparkle

Un fichier qui contient uniquement la logique de mise à jour automatique (par ex., UpdaterCoordinator.swift) reçoit l’intégralité du fichier enveloppée dans #if.

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

@Observable
@MainActor
final class UpdaterCoordinator {
    // Code wrapping the Sparkle updater …
}
#endif

Dans le build MAS, ce fichier compile comme s’il était vide et n’a aucun effet.

Branchement ② — Envelopper également les utilisateurs de Sparkle

Le code qui crée UpdaterCoordinator et le passe à l’interface utilisateur doit également être branché. Sinon le build MAS ferait référence à « un type qui n’existe pas ».

@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: {
            // Menu bar icon …
        }
    }
}

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

    var body: some View {
        Button("Show FocusTimer") { /* … */ }

        #if canImport(Sparkle)
        Button("Check for Updates…") { updater.checkForUpdates() }
            .disabled(!updater.canCheckForUpdates)
        #endif

        // Other shared menu items …
    }
}

Il y a trois points clés.

  • La déclaration du champ updater elle-même est enveloppée dans #if.
  • Tout vue qui reçoit updater (PreferenceView, MenuBarContent) est branchée en deux formes — une pour quand Sparkle est présent et une pour quand il ne l’est pas.
  • Les éléments d’interface utilisateur exclusifs Sparkle comme « Check for Updates… » sont également enveloppés dans #if. Dans le build MAS, cet élément de menu n’apparaît pas du tout — et c’est juste, l’édition App Store ne devrait pas avoir de menu de mise à jour automatique.

Les éléments tels que la bascule « Activer les mises à jour automatiques » dans l’écran de paramètres (PreferenceView) doivent tous être enveloppés avec le même modèle.

Récapitulatif de la Partie 2

Si vous avez suivi jusqu’ici, vous disposez maintenant des éléments suivants :

  • ✅ Un fichier entitlements exclusif MAS FocusTimer-MAS.entitlements (permissions minimales)
  • ✅ Un FocusTimer-MAS-Info.plist exclusif MAS (clés Sparkle supprimées, métadonnées App Store ajoutées)
  • ✅ Les paramètres de build de la cible MAS réglés
  • ✅ Le code de mise à jour automatique branché avec #if canImport(Sparkle)

Maintenant la cible FocusTimer MAS est vraiment différente de la cible de distribution directe — elle est dans une forme qui peut être mise sur l’App Store. Il reste à mettre en place le chemin pour téléverser réellement ce build vers App Store Connect.

Dans la prochaine partie, nous conclurons la série en créant un ExportOptions-MAS.plist pour le téléversement, en enregistrant un enregistrement d’application dans App Store Connect, et en expliquant comment vérifier que les deux canaux restent fonctionnels.