Tornando o target verdadeiramente “específico para MAS”
Na Parte 1, registramos um Bundle ID exclusivo para MAS e duplicamos o target de build FocusTimer MAS. Mas esse target ainda é apenas uma cópia do target de distribuição direta.
Um build MAS precisa diferir do build de distribuição direta em três aspectos.
- Entitlements — apenas o conjunto mínimo adequado para a App Store
- Info.plist — remova as chaves do Sparkle, adicione metadados da App Store
- Código — ramifique para que compile mesmo sem o Sparkle
Neste artigo, vamos separar todos os três.
Como na Parte 1,
FocusTimer,com.example.FocusTimer.mase assim por diante são todos valores de exemplo.
Passo 1 — O arquivo de entitlements exclusivo para MAS
Entitlements é um arquivo que lista quais permissões do sistema um app solicita. No build de distribuição direta, o Sparkle precisava de permissões extras para instalar atualizações, mas o build MAS não tem Sparkle, então essas permissões são desnecessárias.
Crie um novo arquivo FocusTimer-MAS.entitlements na raiz do projeto.
<?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 com o FocusTimer.entitlements da distribuição direta, é assim que difere.
- Sem permissões relacionadas ao Sparkle — As entradas
temporary-exception.mach-lookup.*do build de distribuição direta (a exceção que permite ao Sparkle se comunicar com o helper de instalação) não têm razão de estar no build MAS. - Sem
network.client— Com o Sparkle removido, o próprio app FocusTimer não faz chamadas de rede. É honesto remover permissões que você não usa, e permissões desnecessárias também são sinalizadas durante a revisão. Quanto menor a superfície de permissões que um app solicita, melhor.
O exemplo acima é a forma mais simples possível, com apenas permissão suficiente para “ler e escrever arquivos que o usuário selecionou explicitamente”. Se o seu app realmente usa a rede ou outros recursos, adicione as permissões correspondentes, mas nunca inclua permissões que você não usa.
Passo 2 — O arquivo Info.plist exclusivo para MAS
Da mesma forma, crie um novo FocusTimer-MAS-Info.plist na raiz do projeto.
<?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>
Há duas diferenças em relação ao FocusTimer-Info.plist da distribuição direta.
① Todas as chaves SU* do Sparkle são removidas
O Info.plist da distribuição direta continha chaves de configuração do Sparkle como SUFeedURL e SUPublicEDKey. O Info.plist do MAS não deve conter nenhuma dessas chaves. A revisão da App Store proíbe atualizações automáticas autossuficientes, então até mesmo uma chave que apenas sugere tal recurso permanecendo no plist pode ser um problema. É mais seguro não incluir a chave de forma alguma.
② Chaves de metadados da App Store são adicionadas
LSApplicationCategoryType— a categoria à qual o app pertence na App Store. Opublic.app-category.productivityno exemplo acima significa a categoria “Produtividade”. Esse valor deve corresponder à categoria que você definir no App Store Connect na Parte 3, para que um aviso de incompatibilidade não apareça durante a revisão.ITSAppUsesNonExemptEncryption— se o app usa tecnologia de criptografia sujeita a regulamentações de exportação. Se você usa apenas criptografia padrão (ex.: HTTPS, APIs de sistema padrão) ou nenhuma criptografia, pode definir isso comofalse. Codificar essa chave antecipadamente limpa automaticamente o questionário de criptografia que aparece toda vez que você faz upload de um build.
Antes de definir
ITSAppUsesNonExemptEncryptioncomofalse, verifique se o seu app realmente não usa criptografia não padrão. Se não tiver certeza, é mais seguro consultar a documentação da Apple sobre criptografia.
Passo 3 — Configurações de build do target MAS
Agora para a atualização das configurações de build que adiamos na Parte 1, Seção 2-3. Em TARGETS → FocusTimer MAS → Build Settings, alinhe o seguinte.
| Chave de configuração de build | 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 | Mesmo valor do target de distribuição direta |
Os pontos principais são estes.
INFOPLIST_FILEeCODE_SIGN_ENTITLEMENTSdevem apontar para os arquivos exclusivos para MAS que você acabou de criar. Um erro comum é deixar essas duas linhas apontando para os arquivos da distribuição direta.- Definir
CODE_SIGN_STYLEcomoAutomaticpermite que o Xcode emita e combine automaticamente o perfil de provisionamento da Mac App Store e o certificado Apple Distribution. Você não precisa gerenciar a assinatura manualmente. - Se o seu app não usa a rede, não defina
ENABLE_OUTGOING_NETWORK_CONNECTIONS. Isso tem o mesmo raciocínio que removernetwork.clientdos entitlements. - Mantenha os números de versão (
MARKETING_VERSION, etc.) nos mesmos valores do target de distribuição direta, para que as versões dos dois canais não se desalinhem.
Passo 4 — Ramificando o código do Sparkle (#if canImport(Sparkle))
Mesmo após separar os arquivos de configuração, um problema permanece. Em algum lugar no código-fonte há import Sparkle e código que usa o Sparkle, mas o Sparkle não está vinculado ao target MAS, então a própria instrução import produz um erro de compilação.
A solução é envolver o código relacionado ao Sparkle com a diretiva de compilação condicional do Swift #if canImport(Sparkle).
Por que canImport — por que não usar uma flag separada
Você poderia também ramificar com uma flag de compilação personalizada como #if MAS_BUILD. Mas então você precisaria manter manualmente em sincronia a configuração “ativar a flag para o target MAS, desativar para o target de distribuição direta”. Isso é fácil de ficar fora de sincronia à medida que os targets se multiplicam ou as configurações mudam.
canImport(Sparkle) é diferente. Ele faz o compilador verificar diretamente se “Sparkle.framework está vinculado a este target”. Como removemos o Sparkle das dependências de pacote do target MAS na Parte 1, canImport(Sparkle) é automaticamente falso no target MAS. Não há nenhuma flag separada para manter em sincronia. É por isso que apps macOS usam esse padrão como padrão de fato ao separar um canal MAS.
Ramificação ① — Envolver arquivos exclusivos do Sparkle inteiramente
Um arquivo que contém apenas a lógica de atualização automática (ex.: UpdaterCoordinator.swift) tem o arquivo inteiro envolvido em #if.
#if canImport(Sparkle)
import Foundation
import Sparkle
import Combine
@Observable
@MainActor
final class UpdaterCoordinator {
// Código que envolve o atualizador do Sparkle …
}
#endif
No build MAS, este arquivo é compilado como se estivesse vazio e não tem efeito algum.
Ramificação ② — Envolver também quem usa o Sparkle
O código que cria o UpdaterCoordinator e o passa para a interface também precisa ser ramificado. Caso contrário, o build MAS referenciaria um “tipo que não 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: {
// Ícone da barra de menus …
}
}
}
private struct MenuBarContent: View {
#if canImport(Sparkle)
@Bindable var updater: UpdaterCoordinator
#endif
var body: some View {
Button("Mostrar FocusTimer") { /* … */ }
#if canImport(Sparkle)
Button("Verificar atualizações…") { updater.checkForUpdates() }
.disabled(!updater.canCheckForUpdates)
#endif
// Outros itens de menu compartilhados …
}
}
Há três pontos principais.
- A própria declaração do campo
updateré envolvida em#if. - Qualquer view que recebe
updater(PreferenceView,MenuBarContent) é ramificada em duas formas — uma para quando o Sparkle está presente e outra para quando não está. - Elementos de interface exclusivos do Sparkle como “Verificar atualizações…” também são envolvidos em
#if. No build MAS, esse item de menu não aparece — e com razão, a edição da App Store não deve ter um menu de atualização próprio.
Elementos como o toggle “Ativar atualizações automáticas” na tela de configurações (PreferenceView) devem todos ser envolvidos com o mesmo padrão.
Resumo da Parte 2
Se você acompanhou até aqui, agora tem:
- ✅ Um arquivo de entitlements exclusivo para MAS
FocusTimer-MAS.entitlements(permissões mínimas) - ✅ Um
FocusTimer-MAS-Info.plistexclusivo para MAS (chaves do Sparkle removidas, metadados da App Store adicionados) - ✅ As configurações de build do target MAS ajustadas
- ✅ Código de atualização automática ramificado com
#if canImport(Sparkle)
Agora o target FocusTimer MAS é verdadeiramente diferente do target de distribuição direta — está em uma forma que pode ser colocada na App Store. O que falta é configurar o caminho para realmente fazer o upload desse build para o App Store Connect.
Na próxima parte, concluiremos a série criando um ExportOptions-MAS.plist para upload, registrando um registro de app no App Store Connect e explicando como verificar se os dois canais permanecem íntegros daqui para frente.