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.

  1. Entitlements — apenas o conjunto mínimo adequado para a App Store
  2. Info.plist — remova as chaves do Sparkle, adicione metadados da App Store
  3. 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.mas e 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. O public.app-category.productivity no 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 como false. 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 ITSAppUsesNonExemptEncryption como false, 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 buildValor
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_VERSIONMesmo valor do target de distribuição direta

Os pontos principais são estes.

  • INFOPLIST_FILE e CODE_SIGN_ENTITLEMENTS devem 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_STYLE como Automatic permite 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 remover network.client dos 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.plist exclusivo 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.