Membuat Target Benar-Benar “Khusus MAS”

Di Bagian 1, kita mendaftarkan Bundle ID khusus MAS dan menduplikasi build target FocusTimer MAS. Tetapi target tersebut masih hanya salinan dari target distribusi langsung.

Build MAS harus berbeda dari build distribusi langsung dalam tiga hal.

  1. Entitlement — hanya set minimum yang sesuai untuk App Store
  2. Info.plist — hilangkan kunci Sparkle, tambahkan metadata App Store
  3. Kode — percabangan agar tetap bisa dikompilasi meskipun tanpa Sparkle

Dalam artikel ini, kita akan memisahkan ketiganya.

Seperti di Bagian 1, FocusTimer, com.example.FocusTimer.mas, dan sebagainya semuanya adalah nilai contoh.

Langkah 1 — File Entitlement Khusus MAS

Entitlement adalah file yang mencantumkan izin sistem mana yang diminta oleh aplikasi. Dalam build distribusi langsung, Sparkle memerlukan izin tambahan untuk menginstal pembaruan, tetapi build MAS tidak memiliki Sparkle, sehingga izin tersebut tidak diperlukan.

Buat file FocusTimer-MAS.entitlements baru di root proyek.

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

Dibandingkan dengan FocusTimer.entitlements distribusi langsung, inilah perbedaannya.

  • Tidak ada izin terkait Sparkle — Entri temporary-exception.mach-lookup.* dari build distribusi langsung (pengecualian yang memungkinkan Sparkle berkomunikasi dengan helper installer) tidak ada alasan untuk ada di build MAS.
  • Tidak ada network.client — Dengan Sparkle dihapus, aplikasi FocusTimer itu sendiri tidak membuat panggilan jaringan apa pun. Adalah jujur untuk menghapus izin yang tidak Anda gunakan, dan izin yang tidak perlu juga ditandai selama review. Semakin kecil permukaan izin yang diminta aplikasi, semakin baik.

Contoh di atas adalah bentuk paling sederhana yang mungkin, dengan hanya cukup izin untuk “membaca dan menulis file yang secara eksplisit dipilih pengguna.” Jika aplikasi Anda benar-benar menggunakan jaringan atau sumber daya lain, tambahkan izin yang sesuai, tetapi jangan pernah menyertakan izin yang tidak Anda gunakan.

Langkah 2 — File Info.plist Khusus MAS

Demikian pula, buat FocusTimer-MAS-Info.plist baru di root proyek.

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

Ada dua perbedaan dari FocusTimer-Info.plist distribusi langsung.

① Semua kunci Sparkle SU* dihilangkan

Info.plist distribusi langsung berisi kunci konfigurasi Sparkle seperti SUFeedURL dan SUPublicEDKey. Info.plist MAS tidak boleh mengandung satu pun kunci ini. Review App Store melarang pembaruan otomatis bawaan, sehingga bahkan kunci yang hanya mengisyaratkan fitur semacam itu yang tersisa di plist bisa menjadi masalah. Paling aman untuk tidak menyertakan kuncinya sama sekali.

② Kunci metadata App Store ditambahkan

  • LSApplicationCategoryType — kategori tempat aplikasi berada di App Store. public.app-category.productivity dalam contoh di atas berarti kategori “Produktivitas.” Nilai ini harus cocok dengan kategori yang Anda tetapkan di App Store Connect di Bagian 3, sehingga peringatan ketidakcocokan tidak muncul selama review.
  • ITSAppUsesNonExemptEncryption — apakah aplikasi menggunakan teknologi enkripsi yang tunduk pada peraturan ekspor. Jika Anda hanya menggunakan enkripsi standar (misalnya, HTTPS, API sistem standar) atau tidak menggunakan enkripsi sama sekali, Anda dapat menetapkan ini ke false. Meng-hardcode kunci ini terlebih dahulu secara otomatis melewati kuesioner enkripsi yang muncul setiap kali Anda mengunggah build.

Sebelum menetapkan ITSAppUsesNonExemptEncryption ke false, verifikasi bahwa aplikasi Anda benar-benar tidak menggunakan enkripsi non-standar. Jika Anda tidak yakin, lebih aman untuk merujuk dokumentasi Apple tentang enkripsi.

Langkah 3 — Pengaturan Build Target MAS

Sekarang untuk pembaruan pengaturan build yang kita tunda di Bagian 1, Bagian 2-3. Di TARGETS → FocusTimer MAS → Build Settings, selaraskan hal-hal berikut.

Kunci pengaturan buildNilai
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_VERSIONSama dengan target distribusi langsung

Poin-poin utamanya adalah sebagai berikut.

  • INFOPLIST_FILE dan CODE_SIGN_ENTITLEMENTS harus mengarah ke file khusus MAS yang baru saja Anda buat. Kesalahan umum adalah membiarkan kedua baris ini mengarah ke file distribusi langsung.
  • Menetapkan CODE_SIGN_STYLE ke Automatic membiarkan Xcode secara otomatis menerbitkan dan mencocokkan profil provisioning Mac App Store dan sertifikat Apple Distribution. Anda tidak perlu mengelola penandatanganan sendiri.
  • Jika aplikasi Anda tidak menggunakan jaringan, jangan atur ENABLE_OUTGOING_NETWORK_CONNECTIONS. Ini sama dengan alasan menghapus network.client dari entitlement.
  • Pertahankan nomor versi (MARKETING_VERSION, dll.) pada nilai yang sama dengan target distribusi langsung, agar versi dua saluran tidak menyimpang.

Langkah 4 — Percabangan Kode Sparkle (#if canImport(Sparkle))

Bahkan setelah memisahkan file konfigurasi, satu masalah tetap ada. Di suatu tempat dalam kode sumber terdapat import Sparkle dan kode yang menggunakan Sparkle, tetapi Sparkle tidak ditautkan ke target MAS, sehingga pernyataan import itu sendiri menghasilkan error kompilasi.

Solusinya adalah membungkus kode terkait Sparkle dengan direktif kompilasi kondisional Swift #if canImport(Sparkle).

Mengapa canImport — Mengapa Tidak Menggunakan Flag Terpisah

Anda juga bisa membercabangkan dengan flag kompilasi kustom seperti #if MAS_BUILD. Tetapi kemudian Anda harus secara manual menjaga sinkronisasi pengaturan “aktifkan flag untuk target MAS, matikan untuk target distribusi langsung.” Itu mudah tidak sinkron seiring bertambahnya target atau berubahnya pengaturan.

canImport(Sparkle) berbeda. Ini membuat kompilator langsung memeriksa apakah “Sparkle.framework ditautkan ke target ini.” Karena kita menghapus Sparkle dari dependensi paket target MAS di Bagian 1, canImport(Sparkle) secara otomatis false di target MAS. Tidak ada flag terpisah yang perlu dijaga sinkronisasinya. Itulah mengapa aplikasi macOS menggunakan pola ini sebagai standar de facto saat memisahkan saluran MAS.

Percabangan ① — Bungkus File Khusus Sparkle Seluruhnya

File yang hanya berisi logika pembaruan otomatis (misalnya, UpdaterCoordinator.swift) mendapat seluruh file dibungkus dalam #if.

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

@Observable
@MainActor
final class UpdaterCoordinator {
    // Kode yang membungkus Sparkle updater …
}
#endif

Dalam build MAS, file ini dikompilasi seolah kosong dan tidak berpengaruh sama sekali.

Percabangan ② — Bungkus Juga Pengguna Sparkle

Kode yang membuat UpdaterCoordinator dan meneruskannya ke UI juga perlu dibercabangkan. Jika tidak, build MAS akan mereferensikan “tipe yang tidak ada.”

@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: {
            // Ikon menu bar …
        }
    }
}

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

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

        #if canImport(Sparkle)
        Button("Periksa Pembaruan…") { updater.checkForUpdates() }
            .disabled(!updater.canCheckForUpdates)
        #endif

        // Item menu bersama lainnya …
    }
}

Ada tiga poin utama.

  • Deklarasi field updater itu sendiri dibungkus dalam #if.
  • Setiap tampilan yang menerima updater (PreferenceView, MenuBarContent) dibercabangkan menjadi dua bentuk — satu untuk saat Sparkle ada dan satu untuk saat tidak ada.
  • Elemen UI khusus Sparkle seperti “Periksa Pembaruan…” juga dibungkus dalam #if. Dalam build MAS, item menu ini sama sekali tidak muncul — dan memang benar demikian, edisi App Store seharusnya tidak memiliki menu pembaruan sendiri.

Elemen seperti toggle “Aktifkan pembaruan otomatis” di layar pengaturan (PreferenceView) semuanya harus dibungkus dengan pola yang sama.

Rangkuman Bagian 2

Jika Anda telah mengikuti sejauh ini, Anda sekarang memiliki:

  • ✅ File entitlement khusus MAS FocusTimer-MAS.entitlements (izin minimum)
  • FocusTimer-MAS-Info.plist khusus MAS (kunci Sparkle dihapus, metadata App Store ditambahkan)
  • ✅ Pengaturan build target MAS yang diselaraskan
  • ✅ Kode pembaruan otomatis yang dibercabangkan dengan #if canImport(Sparkle)

Sekarang target FocusTimer MAS benar-benar berbeda dari target distribusi langsung — ini dalam bentuk yang dapat dimasukkan ke App Store. Yang tersisa adalah menyiapkan jalur untuk benar-benar mengunggah build ini ke App Store Connect.

Di bagian berikutnya, kita akan menyelesaikan seri dengan membuat ExportOptions-MAS.plist untuk upload, mendaftarkan catatan aplikasi di App Store Connect, dan membahas cara memverifikasi agar dua saluran tetap tidak rusak ke depannya.