Con Đường Gửi Build Lên App Store
Ở Phần 1 chúng ta đã tạo MAS build target, và ở Phần 2 chúng ta đã tạo các file cấu hình và phân nhánh code phân tách hai kênh. Target FocusTimer MAS hiện ở dạng có thể đưa lên App Store.
Trong phần cuối này, chúng ta sẽ thiết lập con đường để upload build đó lên App Store Connect, và đề cập đến cách xác minh hai kênh để chúng luôn không bị hỏng, kết thúc series.
Như ở Phần 1 và 2,
FocusTimer,com.example.FocusTimer.mas, Team IDABCDE12345, v.v. đều là giá trị mẫu.
Bước 1 — ExportOptions-MAS.plist Để Upload
Trong series phân phối trực tiếp, chúng ta đã lưu ý rằng khi xuất archive để phân phối, lệnh xcodebuild -exportArchive đọc ExportOptions.plist. Kênh MAS cần một file cấu hình xuất riêng biệt.
Tạo ExportOptions-MAS.plist trong thư mục release 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>method</key>
<string>app-store</string>
<key>destination</key>
<string>upload</string>
<key>signingStyle</key>
<string>automatic</string>
<key>teamID</key>
<string>ABCDE12345</string>
</dict>
</plist>
Đây là ý nghĩa của từng khóa.
method=app-store— Có nghĩa là build này được xuất cho kênh Mac App Store. Điều này trái ngược vớideveloper-idcủa kênh phân phối trực tiếp.destination=upload— Làm choxcodebuild -exportArchiveupload artifact đã xuất thẳng đến App Store Connect. Không cần đi qua Transporter hay công cụ upload riêng biệt như trước đây.signingStyle=automatic— Xcode tự động khớp provisioning profile Mac App Store và chứng chỉ Apple Distribution.teamID— Apple Developer Team ID của bạn.
Sau khi tạo file này, bạn tái sử dụng nó cho mọi lần phát hành MAS. Hãy để ExportOptions.plist cho phân phối trực tiếp như cũ, và giữ file này nằm cạnh nó.
Bước 2 — Đăng Ký App Record Trong App Store Connect
Để thực sự nộp ứng dụng để xét duyệt, phải có record ứng dụng trong App Store Connect. Một record là vỏ chứa tất cả thông tin hiển thị trong cửa hàng — tên ứng dụng, mô tả, ảnh chụp màn hình, giá cả, v.v.
Tạo record có thể đợi đến ngay trước lần nộp thực tế đầu tiên. Ở giai đoạn thiết lập một lần, chỉ cần biết trước “những mục nào cần quyết định và như thế nào” là đủ. Đặc biệt, Primary Language bên dưới rất khó hoàn tác sau khi đã đặt, vì vậy hãy quyết định cẩn thận trước.
Trong App Store Connect → My Apps → + → New App, nhập những thông tin sau.
- Platform — Chọn macOS
- Primary Language (Ngôn ngữ chính) — ⚠️ Mục cần quyết định cẩn thận nhất. Sau khi đặt, rất khó thay đổi qua self-service. Nếu bạn có kế hoạch phát hành ứng dụng ở nhiều quốc gia, thường đặt Tiếng Anh (English, U.S.) làm ngôn ngữ chính, vì ngôn ngữ chính là “ngôn ngữ cơ sở hiển thị khi không có bản dịch cho một quốc gia cụ thể”.
- App Name (Tên ứng dụng) — Tên bằng ngôn ngữ chính (ví dụ:
FocusTimer). Tên cho các ngôn ngữ khác được thêm sau riêng biệt như bản địa hóa theo từng ngôn ngữ. Ví dụ, bạn có thể làm cho người dùng Hàn Quốc thấy tên tiếng Hàn và người dùng Nhật Bản thấy tên tiếng Nhật. - Bundle ID — Chọn
com.example.FocusTimer.mastừ dropdown. Đó là ID bạn đã đăng ký ở Phần 1. Đừng nhầm với Bundle ID phân phối trực tiếp (com.example.FocusTimer) — phải là cái có.masđược thêm vào. - SKU — Chuỗi định danh nội bộ chỉ do bạn sử dụng. Nó không được hiển thị trong cửa hàng, vì vậy bạn có thể chọn tự do (ví dụ:
focustimer-mas-001).
Danh mục ứng dụng phải khớp với giá trị bạn đã đặt vào
LSApplicationCategoryTypetrongInfo.plistở Phần 2. Nếu bạn đặtpublic.app-category.productivity(Năng suất) trong plist, bạn cũng phải chọn danh mục “Năng suất” trong App Store Connect để cảnh báo không khớp không xuất hiện trong quá trình xét duyệt.
Bước 3 — Xác Minh Cả Hai Build Kênh
Bây giờ bạn có một codebase với hai build target. Điều này đi kèm chi phí bảo trì. Nếu bạn thường chỉ build kênh phân phối trực tiếp, bạn sẽ không nhận ra nếu MAS target đã bị hỏng âm thầm. Ví dụ, bạn thêm code mới nhưng quên phân nhánh #if canImport(Sparkle), và chỉ MAS build không biên dịch được.
Cách đáng tin cậy nhất để ngăn điều này là archive cả MAS target mỗi khi bạn thực hiện phát hành phân phối trực tiếp. Nếu build thành công, nghĩa là việc phân nhánh của hai kênh vẫn còn nguyên vẹn.
xcodebuild -project FocusTimer.xcodeproj -scheme "FocusTimer MAS" \
-configuration Release \
-destination 'platform=macOS' \
-archivePath build/FocusTimer-MAS.xcarchive \
archive
Nếu bạn thấy điều sau ở cuối đầu ra, việc phân nhánh MAS vẫn ổn.
** ARCHIVE SUCCEEDED **
Tôi khuyên bạn nên đặt lệnh này vào bước cuối cùng của script tự động hóa phát hành để nó tự động chạy mỗi lần phát hành. Ngay cả khi bạn thực sự không upload MAS build lên App Store lúc đó, chỉ cần xác nhận rằng archive thành công là đủ để đảm bảo rằng việc phân nhánh của hai kênh chưa bị hỏng.
Tổng Kết Series — Hoàn Tất Thiết Lập Một Lần Cho Phát Hành MAS
Phần thiết lập một lần qua ba phần để phát hành lên Mac App Store hiện đã hoàn toàn xong. Bạn hiện có:
- ✅ (Phần 1) Bundle ID chỉ dành cho MAS + build target
FocusTimer MASđược nhân đôi - ✅ (Phần 2) Entitlements và Info.plist chỉ dành cho MAS + cài đặt build + phân nhánh code
#if canImport(Sparkle) - ✅ (Phần 3)
ExportOptions-MAS.plistđể upload + cách đăng ký App Store Connect record + xác minh cả hai build kênh
Với điều này, một ứng dụng macOS duy nhất hiện có cả kênh phân phối trực tiếp (Developer ID) và Mac App Store. Để tóm tắt lại cấu trúc cốt lõi:
- Chia sẻ cùng một codebase, nhưng tách thành hai build target.
- Target phân phối trực tiếp bao gồm Sparkle, và MAS target loại trừ nó.
- Sự khác biệt giữa hai target được thể hiện qua các file entitlements, Info.plist và ExportOptions riêng biệt và phân nhánh code
#if canImport(Sparkle).
Thiết lập ban đầu đòi hỏi nhiều công sức, nhưng đây cũng là cấu trúc tiếp tục được tái sử dụng sau khi đã thiết lập xong. Từ đó, khi bạn thực hiện phát hành phân phối trực tiếp, bạn chỉ cần archive MAS build cùng lúc và xác nhận việc phân nhánh vẫn còn hoạt động. Việc nộp App Store thực tế (ảnh chụp màn hình, mô tả, xử lý xét duyệt) là công việc riêng biệt được thực hiện trên phần thiết lập một lần này, và đó là chủ đề cho một bài viết khác.