Convertir el target en uno de verdad “para MAS”
En la Parte 1 registramos un Bundle ID exclusivo para MAS y duplicamos el target de compilación FocusTimer MAS. Pero ese target todavía no es más que una copia del target de distribución directa.
La compilación de MAS debe diferenciarse de la de distribución directa en tres cosas.
- Entitlements — solo los permisos mínimos adecuados para la App Store
- Info.plist — quitar las claves de Sparkle y añadir los metadatos de la App Store
- Código — bifurcado para que compile aunque Sparkle no esté presente
En este artículo separamos las tres.
Igual que en la Parte 1,
FocusTimer,com.example.FocusTimer.mas, etc., son todos valores de ejemplo.
Paso 1 — Archivo de entitlements exclusivo para MAS
Los entitlements son un archivo donde se anotan qué permisos del sistema solicita la app. En la compilación de distribución directa, Sparkle requería permisos adicionales para instalar las actualizaciones, pero como la compilación de MAS no tiene Sparkle, esos permisos son innecesarios.
Crea un archivo nuevo FocusTimer-MAS.entitlements en la raíz del proyecto.
<?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>
Comparado con el FocusTimer.entitlements de distribución directa, las diferencias son estas.
- No hay permisos relacionados con Sparkle — el
temporary-exception.mach-lookup.*que estaba en la compilación de distribución directa (una excepción para que Sparkle se comunique con el helper de instalación) no tiene razón de estar en la compilación de MAS. - No hay
network.client— si se quita Sparkle, el núcleo de FocusTimer no hace ninguna llamada de red. Lo honesto es quitar los permisos que no se usan, y en la revisión también se señalan los permisos innecesarios. Cuanto más pequeña sea la superficie de permisos que solicita la app, mejor.
El ejemplo de arriba es la forma más sencilla, con solo el permiso de “leer y escribir los archivos que el usuario elija directamente”. Si tu app usa de verdad la red u otros recursos, añade los permisos correspondientes, pero nunca incluyas permisos que no uses.
Paso 2 — Archivo Info.plist exclusivo para MAS
Del mismo modo, crea un archivo nuevo FocusTimer-MAS-Info.plist en la raíz del proyecto.
<?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>
Las diferencias con el FocusTimer-Info.plist de distribución directa son dos.
① Desaparecen todas las claves SU* de Sparkle
El Info.plist de distribución directa contenía claves de configuración de Sparkle como SUFeedURL o SUPublicEDKey. El Info.plist para MAS no debe tener ninguna de estas claves. Como la revisión de la App Store prohíbe los sistemas de actualizaciones automáticas propios, el simple hecho de que en el plist quede una clave que insinúe esa funcionalidad ya puede ser un problema. Lo más seguro es no incluir las claves en absoluto.
② Se añaden claves de metadatos de la App Store
LSApplicationCategoryType— la categoría a la que pertenecerá la app en la App Store. Elpublic.app-category.productivitydel ejemplo de arriba significa la categoría “Productividad”. Este valor debe coincidir con la categoría que configurarás en App Store Connect en la Parte 3, para que en la fase de revisión no aparezca una advertencia de discrepancia.ITSAppUsesNonExemptEncryption— indica si la app usa tecnología de cifrado sujeta a restricciones de exportación. Si solo usas cifrado común (por ejemplo, HTTPS o APIs estándar del sistema) o no usas cifrado, puedes dejarlo enfalse. Si fijas esta clave por adelantado, pasarás automáticamente el cuestionario de cifrado que aparece cada vez que subes una compilación.
Antes de dejar
ITSAppUsesNonExemptEncryptionenfalse, comprueba si tu app realmente no usa cifrado no estándar. Si no lo tienes claro, lo más seguro es consultar la documentación de Apple sobre cifrado.
Paso 3 — Ajustes de compilación del target MAS
Ahora viene la actualización de los ajustes de compilación que pospusimos en el apartado 2-3 de la Parte 1. En TARGETS → FocusTimer MAS → Build Settings ajusta lo siguiente.
| Clave de ajuste de compilación | Valor |
|---|---|
PRODUCT_BUNDLE_IDENTIFIER | com.example.FocusTimer.mas |
INFOPLIST_FILE | FocusTimer-MAS-Info.plist |
CODE_SIGN_ENTITLEMENTS | FocusTimer-MAS.entitlements |
ENABLE_APP_SANDBOX | YES |
ENABLE_HARDENED_RUNTIME | YES |
ENABLE_USER_SELECTED_FILES | readwrite |
CODE_SIGN_STYLE | Automatic |
MARKETING_VERSION, CURRENT_PROJECT_VERSION | Iguales que en el target de distribución directa |
Lo esencial es esto.
INFOPLIST_FILEyCODE_SIGN_ENTITLEMENTSdeben apuntar a los archivos exclusivos para MAS que acabas de crear. Es un error frecuente dejar estas dos líneas con los archivos de distribución directa tal cual.- Si dejas
CODE_SIGN_STYLEenAutomatic, Xcode emite y empareja automáticamente el perfil de aprovisionamiento para la Mac App Store y el certificado de Apple Distribution. No necesitas gestionar la firma manualmente. - Si tu app no usa la red, no configures
ENABLE_OUTGOING_NETWORK_CONNECTIONS. Va en la misma línea que haber quitadonetwork.clientde los entitlements. - Mantén el número de versión (
MARKETING_VERSION, etc.) con el mismo valor que el target de distribución directa, para que las versiones de ambos canales no se desincronicen.
Paso 4 — Bifurcar el código de Sparkle (#if canImport(Sparkle))
Aunque hayas separado los archivos de configuración, queda un problema. En algún punto del código fuente habrá un import Sparkle y código que usa Sparkle, pero como en el target MAS Sparkle no se enlaza, esa propia sentencia import produce un error de compilación.
La solución es envolver el código relacionado con Sparkle con la directiva de compilación condicional de Swift #if canImport(Sparkle).
Por qué canImport — el motivo de no usar un flag aparte
También podrías bifurcar con un flag de compilación creado por ti como #if MAS_BUILD. Pero entonces tendrías que sincronizar manualmente la configuración de “activar ese flag en el target MAS y desactivarlo en el target de distribución directa”. Es fácil que se desincronice cuando aumentan los targets o cambia la configuración.
canImport(Sparkle) es distinto. Esto hace que el propio compilador compruebe directamente “si Sparkle.framework está enlazado en este target”. Como en la Parte 1 eliminamos Sparkle de las dependencias de paquetes del target MAS, en el target MAS canImport(Sparkle) es automáticamente falso (false). No hay ningún flag aparte que sincronizar. Por eso las apps de macOS usan este patrón casi como un estándar cuando separan el canal MAS.
Bifurcación ① — los archivos exclusivos de Sparkle se envuelven enteros
Un archivo que solo contiene la lógica de actualizaciones automáticas (por ejemplo, UpdaterCoordinator.swift) se envuelve por completo con #if.
#if canImport(Sparkle)
import Foundation
import Sparkle
import Combine
@Observable
@MainActor
final class UpdaterCoordinator {
// Código que envuelve el actualizador de Sparkle …
}
#endif
En la compilación de MAS este archivo se compila como si fuera un archivo vacío y no tiene ningún efecto.
Bifurcación ② — también se envuelve la parte que usa Sparkle
El código que crea UpdaterCoordinator y lo pasa a la pantalla también hay que bifurcarlo. Si no, en la compilación de MAS se acabaría haciendo referencia a un “tipo que no existe”.
@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: {
// Icono de la barra de menús …
}
}
}
private struct MenuBarContent: View {
#if canImport(Sparkle)
@Bindable var updater: UpdaterCoordinator
#endif
var body: some View {
Button("Mostrar FocusTimer") { /* … */ }
#if canImport(Sparkle)
Button("Buscar actualizaciones…") { updater.checkForUpdates() }
.disabled(!updater.canCheckForUpdates)
#endif
// Resto de menús comunes …
}
}
Los puntos clave son tres.
- La propia declaración del campo
updaterse envuelve con#if. - Las pantallas que reciben
updater(PreferenceView,MenuBarContent) se bifurcan en dos formas, una para cuando Sparkle está presente y otra para cuando no. - Los elementos de interfaz exclusivos de Sparkle como “Buscar actualizaciones…” también se envuelven con
#if. En la compilación de MAS este menú no aparece en absoluto, y es lo correcto: la versión de la App Store no tiene un menú de actualización propio.
Los elementos de la pantalla de preferencias (PreferenceView), como el conmutador de “Activar actualizaciones automáticas”, también se envuelven todos con el mismo patrón.
Resumen de la Parte 2
Si has seguido hasta aquí, ahora tienes lo siguiente.
- ✅ El archivo de entitlements exclusivo para MAS
FocusTimer-MAS.entitlements(permisos mínimos) - ✅ El
FocusTimer-MAS-Info.plistexclusivo para MAS (claves de Sparkle eliminadas, metadatos de la App Store añadidos) - ✅ Los ajustes de compilación del target MAS ordenados
- ✅ El código de actualizaciones automáticas bifurcado con
#if canImport(Sparkle)
Ahora el target FocusTimer MAS se ha convertido en algo de verdad distinto del target de distribución directa: una forma que se puede subir a la App Store. Lo que queda es preparar el camino para subir realmente esta compilación a App Store Connect.
En la siguiente parte cerraremos la serie creando un ExportOptions-MAS.plist para la subida, registrando un registro de la app en App Store Connect y viendo cómo verificar que ambos canales no se rompan en el futuro.