Làm Cho Target Thực Sự “Dành Riêng Cho MAS”
Ở Phần 1, chúng ta đã đăng ký Bundle ID chỉ dành cho MAS và nhân đôi build target FocusTimer MAS. Nhưng target đó vẫn chỉ là bản sao của target phân phối trực tiếp.
MAS build phải khác với build phân phối trực tiếp ở ba điểm.
- Entitlements — chỉ bộ quyền hạn tối thiểu phù hợp cho App Store
- Info.plist — bỏ các khóa Sparkle, thêm metadata App Store
- Code — phân nhánh để biên dịch được ngay cả khi không có Sparkle
Trong bài viết này, chúng ta sẽ tách cả ba điều đó.
Như ở Phần 1,
FocusTimer,com.example.FocusTimer.mas, v.v. đều là giá trị mẫu.
Bước 1 — File Entitlements Chỉ Dành Cho MAS
Entitlements là file liệt kê các quyền hệ thống mà ứng dụng yêu cầu. Trong build phân phối trực tiếp, Sparkle cần các quyền bổ sung để cài đặt bản cập nhật, nhưng MAS build không có Sparkle, vì vậy những quyền đó là không cần thiết.
Tạo file FocusTimer-MAS.entitlements mới trong thư mục gốc của dự án.
<?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>
So với FocusTimer.entitlements cho phân phối trực tiếp, đây là sự khác biệt.
- Không có quyền liên quan đến Sparkle — Các mục
temporary-exception.mach-lookup.*từ build phân phối trực tiếp (ngoại lệ cho phép Sparkle giao tiếp với installation helper) không có lý do gì để tồn tại trong MAS build. - Không có
network.client— Khi Sparkle bị xóa, bản thân ứng dụng FocusTimer không thực hiện bất kỳ cuộc gọi mạng nào. Thành thật mà bỏ đi các quyền bạn không dùng, và các quyền không cần thiết cũng bị gắn cờ trong quá trình xét duyệt. Bề mặt quyền hạn ứng dụng yêu cầu càng nhỏ càng tốt.
Ví dụ trên là dạng đơn giản nhất có thể, chỉ đủ quyền để “đọc và ghi các file mà người dùng chọn rõ ràng”. Nếu ứng dụng của bạn thực sự dùng mạng hay tài nguyên khác, hãy thêm quyền tương ứng, nhưng không bao giờ bao gồm quyền bạn không dùng.
Bước 2 — File Info.plist Chỉ Dành Cho MAS
Tương tự, tạo FocusTimer-MAS-Info.plist mới trong thư mục gốc của dự án.
<?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>
Có hai điểm khác biệt so với FocusTimer-Info.plist cho phân phối trực tiếp.
① Tất cả các khóa Sparkle SU* đều bị bỏ
Info.plist cho phân phối trực tiếp chứa các khóa cấu hình Sparkle như SUFeedURL và SUPublicEDKey. Info.plist MAS không được chứa bất kỳ khóa nào trong số này. Việc xét duyệt App Store cấm cập nhật tự động tích hợp sẵn, vì vậy ngay cả một khóa chỉ gợi ý đến tính năng như vậy còn lại trong plist cũng có thể là vấn đề. An toàn nhất là không đưa khóa vào luôn.
② Các khóa metadata App Store được thêm vào
LSApplicationCategoryType— danh mục ứng dụng thuộc về trên App Store.public.app-category.productivitytrong ví dụ trên có nghĩa là danh mục “Năng suất”. Giá trị này phải khớp với danh mục bạn đặt trong App Store Connect ở Phần 3, để cảnh báo không khớp không xuất hiện trong quá trình xét duyệt.ITSAppUsesNonExemptEncryption— liệu ứng dụng có sử dụng công nghệ mã hóa tuân theo quy định xuất khẩu hay không. Nếu bạn chỉ dùng mã hóa tiêu chuẩn (ví dụ: HTTPS, API hệ thống tiêu chuẩn) hoặc không dùng mã hóa, bạn có thể đặt thànhfalse. Nhúng cứng khóa này trước sẽ tự động vượt qua bảng câu hỏi mã hóa xuất hiện mỗi khi bạn upload build.
Trước khi đặt
ITSAppUsesNonExemptEncryptionthànhfalse, hãy xác nhận rằng ứng dụng của bạn thực sự không dùng mã hóa phi tiêu chuẩn. Nếu bạn không chắc, an toàn hơn khi tham khảo tài liệu của Apple về mã hóa.
Bước 3 — Cài Đặt Build MAS Target
Bây giờ là cập nhật cài đặt build mà chúng ta đã trì hoãn từ Phần 1, Mục 2-3. Trong TARGETS → FocusTimer MAS → Build Settings, hãy căn chỉnh những thứ sau.
| Khóa cài đặt build | Giá trị |
|---|---|
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 | Giống như target phân phối trực tiếp |
Các điểm mấu chốt như sau.
INFOPLIST_FILEvàCODE_SIGN_ENTITLEMENTSphải trỏ đến các file chỉ dành cho MAS bạn vừa tạo. Lỗi thường gặp là để hai dòng này trỏ đến các file phân phối trực tiếp.- Đặt
CODE_SIGN_STYLEthànhAutomaticđể Xcode tự động cấp và khớp provisioning profile Mac App Store và chứng chỉ Apple Distribution. Bạn không cần tự quản lý việc ký. - Nếu ứng dụng không dùng mạng, không đặt
ENABLE_OUTGOING_NETWORK_CONNECTIONS. Đây là cùng lý do như việc bỏnetwork.clientkhỏi entitlements. - Giữ số phiên bản (
MARKETING_VERSION, v.v.) ở cùng giá trị với target phân phối trực tiếp, để phiên bản của hai kênh không bị lệch nhau.
Bước 4 — Phân Nhánh Code Sparkle (#if canImport(Sparkle))
Ngay cả sau khi tách các file cấu hình, vẫn còn một vấn đề. Đâu đó trong mã nguồn có import Sparkle và code sử dụng Sparkle, nhưng Sparkle không được liên kết vào MAS target, vì vậy chính câu lệnh import đó tạo ra lỗi biên dịch.
Giải pháp là bọc code liên quan đến Sparkle bằng chỉ thị biên dịch có điều kiện của Swift #if canImport(Sparkle).
Tại Sao canImport — Tại Sao Không Dùng Flag Riêng
Bạn cũng có thể phân nhánh bằng flag biên dịch tùy chỉnh như #if MAS_BUILD. Nhưng khi đó bạn sẽ phải đồng bộ thủ công cài đặt “bật flag cho MAS target, tắt cho direct distribution target”. Điều đó dễ bị lệch khi target nhân lên hoặc cài đặt thay đổi.
canImport(Sparkle) thì khác. Nó có trình biên dịch trực tiếp kiểm tra xem “Sparkle.framework có được liên kết vào target này không”. Vì chúng ta đã xóa Sparkle khỏi package dependency của MAS target ở Phần 1, canImport(Sparkle) tự động là false trong MAS target. Không cần flag riêng để đồng bộ. Đó là lý do tại sao các ứng dụng macOS dùng pattern này như tiêu chuẩn thực tế khi tách kênh MAS.
Phân Nhánh ① — Bọc Toàn Bộ File Chỉ Dành Cho Sparkle
File chỉ chứa logic cập nhật tự động (ví dụ: UpdaterCoordinator.swift) được bọc toàn bộ file bằng #if.
#if canImport(Sparkle)
import Foundation
import Sparkle
import Combine
@Observable
@MainActor
final class UpdaterCoordinator {
// Code bọc Sparkle updater …
}
#endif
Trong MAS build, file này được biên dịch như thể nó trống và không có tác dụng gì cả.
Phân Nhánh ② — Bọc Cả Những Nơi Dùng Sparkle
Code tạo UpdaterCoordinator và truyền nó cho UI cũng cần được phân nhánh. Nếu không, MAS build sẽ tham chiếu đến “loại không tồn tại”.
@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: {
// Biểu tượng menu bar …
}
}
}
private struct MenuBarContent: View {
#if canImport(Sparkle)
@Bindable var updater: UpdaterCoordinator
#endif
var body: some View {
Button("Hiển thị FocusTimer") { /* … */ }
#if canImport(Sparkle)
Button("Kiểm tra cập nhật…") { updater.checkForUpdates() }
.disabled(!updater.canCheckForUpdates)
#endif
// Các mục menu chung khác …
}
}
Có ba điểm mấu chốt.
- Chính khai báo field
updaterđược bọc trong#if. - Bất kỳ view nhận
updaternào (PreferenceView,MenuBarContent) đều được phân nhánh thành hai dạng — một khi có Sparkle và một khi không có. - Các phần tử UI chỉ dành cho Sparkle như “Kiểm tra cập nhật…” cũng được bọc trong
#if. Trong MAS build, mục menu này không xuất hiện gì cả — và đúng vậy, phiên bản App Store không nên có menu tự cập nhật.
Các phần tử như toggle “Bật cập nhật tự động” trong màn hình cài đặt (PreferenceView) cũng nên được bọc bằng cùng pattern.
Tổng Kết Phần 2
Nếu bạn đã làm theo đến đây, bây giờ bạn có:
- ✅ File entitlements chỉ dành cho MAS
FocusTimer-MAS.entitlements(quyền hạn tối thiểu) - ✅
FocusTimer-MAS-Info.plistchỉ dành cho MAS (đã xóa khóa Sparkle, đã thêm metadata App Store) - ✅ Cài đặt build của MAS target đã được sắp xếp gọn gàng
- ✅ Code cập nhật tự động được phân nhánh bằng
#if canImport(Sparkle)
Bây giờ target FocusTimer MAS thực sự khác với target phân phối trực tiếp — nó ở dạng có thể đưa lên App Store. Điều còn lại là thiết lập con đường để thực sự upload build này lên App Store Connect.
Ở phần tiếp theo, chúng ta sẽ kết thúc series bằng cách tạo ExportOptions-MAS.plist để upload, đăng ký app record trong App Store Connect, và hướng dẫn cách xác minh để hai kênh luôn không bị hỏng.