上一篇文章中,我介紹了用 iOS(UIKit)+ FastAPI + Kubernetes 打造的 Minecraft 伺服器監控應用程式「MineWatch」的整體設計。那篇文章最後我這樣寫:
若審查通過,之後也會把審查中被指出的地方,以及實際送審流程的細節整理成另一篇文章。
之後,因為 Guideline 5.1.2(i)(追蹤)被拒了 2 次、4.1(c)(Copycat)被拒了 1 次,總共送審 3 次後才終於通過。這篇文章不是接著介紹應用程式功能,而是記錄 實際審查中被指正了什麼,以及我是怎麼修正的。
willConnectTo 呼叫,必須在 sceneDidBecomeActive 才會跳出對話框,這是只能在實機上踩到的坑。MineWatch 為了讓使用者決定是否送出匿名使用狀況(Firebase Analytics)與當機回報(Firebase Crashlytics),實作了自訂同意畫面。字句上刻意避開了「追蹤」「追蹤行為」「廣告」「個人化」等字眼,並且原本以為已經具體說明了收集哪些資訊、用途是什麼(只用於品質改善)。
即便如此,還是因為 Guideline 5.1.2(i) 連續被拒了 2 次。就算改文案也沒用,所以我開始改變方針,懷疑的是實作本身(自訂同意畫面,以及所使用的 SDK 組成)。
原因有兩個:
FirebaseAnalytics 套件,即使實際上完全沒有呼叫 IDFA,也會把 GoogleAppMeasurementIdentitySupport(IDFA 相關程式碼)包含到二進位檔裡。Apple 的靜態分析可能會判定成「具有追蹤功能但未實作 ATT」。因此我改成使用不含 IDFA 的 FirebaseAnalyticsCore product,並透過乾淨重建確認 GoogleAppMeasurementIdentitySupport.framework 沒有被包含進去。到這裡是第二次送審。
除了上面那件事之外,副標題中的 Minecraft伺服器的存活監控 也因為 Guideline 4.1(c) 被拒。理由是「在副標題中使用品牌名(Minecraft)會被判定為 Copycat」。在內文說明中提到「支援 Minecraft 伺服器」是作為相容性說明而被接受的,但 副標題這種直接影響搜尋與列表顯示的位置使用品牌名是不行的。後來改成 遊戲伺服器的存活監控 就解決了。
經過連續 2 次拒審後,我把自訂同意畫面完全移除,改成只使用 Apple 的 ATT 系統對話框。實作上做了以下 3 件事:
ConsentViewController(自訂同意畫面)TrackingPermission,薄薄包裝 ATTrackingManager,並在啟動後立刻呼叫。中間不插入其他說明畫面(避免被視為雙重提示)這裡有個實作上值得記錄的矛盾。MineWatch 實際上沒有使用 IDFA,也沒有做 Apple 定義的「追蹤」行為。 跳出 ATT 對話框本身,就形成了「明明不追蹤,卻還要請求允許」這種看起來不自然的狀況;但考量到前面已經連續 2 次被拒,我優先採取 Apple 在回覆中要求的字面修正。這個決定也一路影響到後面 App Privacy 申報必須重新整理。
willConnectTo 裡不會跳出對話框我把 ATT 的允許對話框寫在 scene(_:willConnectTo:options:) 中,先在模擬器測試,表面上看起來沒問題;但 在實機(iPhone 15 Plus)上測試時,對話框完全沒有出現。
原因是,willConnectTo 當下 scene 還沒有切換到 .active。ATTrackingManager.requestTrackingAuthorization() 就算在非 active 狀態呼叫,也不會丟出例外,而是悄悄回傳 .notDetermined,所以很容易看不出來。
// NG:scene 還沒進入 .active,因此
// 會呼叫對話框但不會顯示
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options: UIScene.ConnectionOptions) {
Task { await TrackingPermission.requestIfNeeded() }
}
// OK
func sceneDidBecomeActive(_ scene: UIScene) {
Task { await TrackingPermission.requestIfNeeded() }
}
更麻煩的是,從 CLI 啟動 App(xcrun simctl launch 或 xcrun devicectl device process launch)時,即使把程式改對了也很難重現。CLI 啟動不會產生和「點圖示啟動」完全相同的前景切換,所以就算改到 sceneDidBecomeActive,也無法用這種方式驗證。一定要真的在主畫面點 App 圖示啟動,才看得到這個問題,也才能確認修正成功。
在實機驗證 ATT 對話框時,我還發現了另一個 bug。App Attest(用來驗證請求是否來自真正的 App)失敗了,但 App 端卻顯示「請重新登入」。
後端(FastAPI)當時把 Firebase ID token 驗證失敗(也就是「誰」不明)與 App Check 驗證失敗(也就是「是不是正牌 App」不明)都回傳成 401。iOS 端無法區分這兩種情況,因此對 App Check 造成的失敗也一律提示使用者重新登入,結果使用者就算重新登入也沒用。
後來我把 App Check 失敗的回應獨立成 403。
# 401:不知道「誰」是誰(Firebase ID token 驗證失敗)
# → 重新登入有可能修好
# 403:不知道是不是「正牌 App」
# → 重新登入也沒用,需要其他說明
iOS 端也配合新增了 RepositoryError 的 .appCheckFailed case,讓它顯示和 401 不同的訊息。把狀態碼的意義切分清楚雖然是很細瑣的工作,但對於避免 client 端出現「錯誤引導」非常有效。
在處理 ATT 的同時,我也碰到 Google 登入失敗,顯示 錯誤 400: invalid_request / We cannot verify the authenticity of this app 的情況。起初我以為是 OAuth 同意畫面的品牌驗證沒過(像是應用程式名稱不一致、首頁沒有充分說明用途),所以在網站上新增了「什麼是 MineWatch」區塊,並通過 Search Console 驗證,原以為已經解決。
但即使品牌驗證完成後,同樣的錯誤還是沒有消失。檢查 Google Cloud Console 的「專案診斷」後,發現 「使用安全的流程」 這一項有警告。
查閱 Google 官方文件後發現,iOS App 要在這一項被判定為「安全」,條件之一就是 實作並強制啟用 App Check。也就是說,除了 Apple 的 ATT 之外,App Check 是否正常運作,也會影響 Google 登入能不能成功。
在這個期間,由於我在實機上反覆重新安裝測試,App Attest 的驗證嘗試次數已經達到 Apple 的上限,導致 App Check 的 token 一直取得失敗(App attestation failed. Too many attempts.)。我透過讓 Debug 版在實機上也像模擬器一樣使用 App Check Debug Provider 來避開這個問題(Release 版仍然使用真正的 App Attest,所以正式版的安全等級沒有改變)。
原本我一直以為 App Check 只是 Firebase 的功能,但這次才發現,它其實也會被 Google 的外部服務(這裡是 OAuth 同意畫面的審查邏輯)引用,是比想像中更橫向的一套機制。
第三次送審最後卡住的是 App Store Connect 的 App Privacy(營養標示)申報。當我試著在二進位檔中已包含 NSUserTrackingUsageDescription 的情況下,申報「不會追蹤」時,ASC 跳出以下訊息阻止我:
應用程式包含 NSUserTrackingUsageDescription,這表示它可能會要求使用者允許追蹤。若要提交審查,請更新應用程式的隱私權回答,指出此 App 收集的資料會用於追蹤目的;或更新應用程式二進位檔並上傳新的建置版本。
前面提到的「實際上沒有追蹤,卻又跳出 ATT 對話框」這個矛盾,在這裡真的成了實際阻礙。若撤回 ATT 實作,很可能又回到 5.1.2(i) 的拒審;因此最後我選擇 把使用狀況資料(Firebase Analytics)重新申報為「用於追蹤你的資料」。這個判斷是基於實務經驗:即使 SDK 構成中沒有 IDFA,Google Analytics 系列 SDK 也常常會被 Apple 判定為符合追蹤定義。
另外在申報過程中,我也發現 當機資料不小心被勾成「用於追蹤目的」,所以把它取消了。當機回報和 ATT 的允許狀態無關,是獨立的開關,因此也必須和 Review Notes 裡「不屬於追蹤,因此不在 ATT 範圍內」的說明一致。
第三次送審後終於順利通過,MineWatch 也成功上架到 App Store。
回頭看,最大的收穫是 Apple、Google 與自家後端這三套審查/驗證機制,其實比想像中還緊密地交錯在一起。
比起「做出 App」,「持續讓 App 通過審查」反而更難。上一篇文章最後那句話,在這次被拒了 3 次之後,變得更加切身有感。
即使沒有經驗也能學會!一起挑戰吧
我也有在寫 note ↓
原文出處:https://qiita.com/jqit-yukiono/items/91755838ea8589fde90d