ARTICLE · 1035512
审核备注不是文档,是快照:一次 24 天过审的复盘

Fingertips 在 9 月 5 日于 iPhone 和 iPad 上架,9 月 6 日登上 Mac。下面是完整的时间表,因为这张时间表的形状本身就是重点:
| macOS 被拒 | |
| 2.4.5 申诉被驳回。 | |
| iOS 被拒 | |
| macOS 过审,提交后仅一天 | |
从首次提交到首次过审,用了 24 天。最后一轮,只用了 1 天。
在没人看之前,我写了什么
我预判有两条准则会出问题,所以在提交之前,为它们都写了备注。
第一条是 2.4.5。Mac 版 App 原本会帮你粘贴:你按下快捷键,输入几个字母,再按回车,代码片段就出现在你正在写的任何地方,因为 App 用 CGEvent.post 注入了一次合成的 Command-V。macOS 把这项能力放在一个权限后面,归类为辅助功能(Accessibility)。
第二条是 3.1.1,关于试用。14 天免费,然后一次性解锁,不搞订阅。
两条备注都写得很详细,而且我当时都认为,它们显然是对的。结果 macOS 以三条准则被拒,其中就包括 2.4.5。
我提起了申诉。8 月 27 日,裁决被维持,我删掉了 Mac 版 App 赖以为生的核心功能。
第一个教训,代价是一周:审核备注没法靠讲道理帮你拿到权限。 2.4.5(iv) 管的是权限被用来做什么,不是你把话说得多清楚。那段文字无论怎么写都不会有用,写得更漂亮从来不是答案。
导致被拒的那一段
这一段是我最想让别的开发者看到的,因为它和人们写审核备注时担心的事,正好相反。
9 月 2 日,距首次提交 21 天,真正被拒的那个构建已经在队列里排了 13 天,iOS 构建终于以 2.1(b) 被退回:_“我们无法购买内购项目(IAP)。”_ 审核设备是一台 iPhone 17 Pro Max。
其实什么都没坏。付费应用协议(Paid Applications Agreement)生效中,8 月 12 日这个产品真的在沙盒环境完成过一次购买,套餐和价格方案都正确,内购也挂在了这次提交上。购买本身是通的。
购买按钮在一个侧边栏里,而 iPhone 的布局从来不会渲染这个侧边栏。就一个分支,if isSideWindow { palette } else { splitLibrary },而 iPhone 永远是紧凑布局,所以唯一能走到付款的入口,是一个没有文字标注的溢出菜单。
而我的审核备注,让审核员去侧边栏里找。 我写了一段话,把他们指到一个在他们手里的设备上根本不存在的地方。我本来想帮忙的备注,却把他们带到了一个不可能装下他们要找的东西的界面。
修复方案根本不在购买流程里:
// First item, above the palette, and it removes itself once bought.if entitlement.state != .purchased {Button(entitlement.menuBarBuyTitle) { AppDelegate.showPaywall() }Divider()}Button("Open Palette ⌥Space") { PaletteController.shared.show() }这是 Mac 的菜单栏。iPhone 用同样的处理,做成一个 safeAreaInset 固定在列表上方。两个入口共用同一个标题属性,所以两端不会出现剩余天数显示不一致的情况。
重新提交时附上的备注,全文
下面是随过审构建一起提交的 iOS 审核备注全文。没有摘要,没有整理。
如何找到购买入口:请务必阅读,这是 build 22 做错的地方。Build 22 因审核员无法购买而被 2.1(b) 拒回。购买本身没有问题,问题在于在iPhone 上找不到。购买控件位于侧边栏,而 iPhone 布局从不渲染侧边栏,只留下一个溢出菜单。我们自己的审核备注让情况更糟,因为它让你去“侧边栏”里找。在 build 24 中,iPhone 上以及 iPad 的侧边窗口里,一行写着“购买 Fingertips,剩余 14 天”的文字,会在 App 一打开时就固定在屏幕顶部。没有菜单,不用滚动。点一下,购买面板就会弹出,显示真实商店价格,带购买、恢复购买和兑换代码三个按钮。在全宽 iPad 和 Mac 上,同一行是侧边栏的第一项。不需要账号、登录或演示凭据。Fingertips 完全没有账号体系。首次启动时 14 天试用即生效,所有功能都可用,所以购买入口立刻可达,而不是要等到试用期结束。下面是 macOS 备注的开头部分,这一份要证明功能已经移除:
自动粘贴功能已移除:请先阅读。本 App 的早期构建曾用 CGEvent.post 注入合成的 Command-V,让选中的片段落到用户正在使用的 App 里。该功能于 2026 年 8 月 20 日因违反准则 2.4.5 被拒。我们提出申诉,8 月 27 日裁决被维持,功能已删除。这一点可以验证,不是空口声明。对已发布的可执行文件运行 "nm -u",针对 AX、CGEvent 或 PostEvent 的搜索结果为空。没有 CGEvent.post,没有CGRequestPostEventAccess,没有任何辅助功能 API 调用,App 也不请求任何辅助功能权限。Mac/Paster.swift 现在只有 40 行:写入剪贴板,然后把焦点还给之前的 App。这份备注,有两点值得说。
第一句话就承认了被拒。 我原本以为这样做很糟,按我的理论,你不该给审核员一个起疑的理由。现在我想法相反:审核员反正能看到被拒历史,主动把它放在开头,决定了这份备注读起来像辩解,还是像一份状态报告。
它给审核员一条命令,而不是一个承诺。 未定义符号(undefined symbol)是二进制从系统导入的东西,所以如果 App 不能调用 CGEvent.post,它就不可能藏着一个调用。任何人都可以对一个二进制跑这条命令,我的、你的都行:
nm -u YourApp.app/Contents/MacOS/YourApp | grep -E '_AX|CGEvent|PostEvent'用带下划线前缀的 -E,不要用 -Ei。 我这篇文章早前的一稿里就用了忽略大小写,那比没用还糟:AX 会连 Axis.Set 和 maxWidth 一起匹配上,在我测试的那个二进制上,它返回了 21 行 SwiftUI。一个对全世界所有 App 都会触发的检查,根本不是检查。
下面用严格形式,分别对那个还带自动粘贴的构建,和我这周提交的构建跑了一遍:
$ nm -u Fingertips.app/Contents/MacOS/Fingertips | grep -E '_AX|CGEvent|PostEvent'# build 22_CGEventCreateKeyboardEvent_CGEventPost_CGEventSetFlags_CGEventSourceCreate_CGPreflightPostEventAccess_CGRequestPostEventAccess$ nm -u Fingertips.app/Contents/MacOS/Fingertips | grep -E '_AX|CGEvent|PostEvent'# build 26$六个符号,然后一个都没有。这就是全部论据,而且十秒钟就能验证。
提前预防有效吗?部分有效,但不在我预期的地方
诚实地打分,因为一份只报喜的复盘,就是广告。
2.4.5:备注完全失败。 提交之前就写好,写得详细,对 App 行为的描述也准确,可拒绝还是来了。后来申诉也失败了。
3.1.1,试用条款:从未被提起过,两个平台、每一轮都没有。 我没法证明是备注阻止了它,也不会这样声称。它只花一段文字,却消灭了整整一类问题,这笔交易很划算,虽然这场赌注永远没法验证。
2.1(b):我的备注帮了倒忙。 它指向一个审核设备根本不渲染的侧边栏。
重写后的备注,第一行就写明此前的拒绝和修复:iOS 3 天过审,Mac 1 天过审,没有追问。 Mac 在提交后 7 小时内就进入审核,而第一轮等了 13 天。
关于最后一行,我想谨慎一点。这是一款 App,样本量是 1,九月的队列更快,并不能证明我的那段文字起了作用,而且还有一个我排除不了、更朴素的解释:被拒版本重新提交,和首次提交走的是不同的队列,第二轮很快,本来就是常见形态。我能说的更窄一些,但仍然有用:回去的构建在任何布局下都藏不住购买入口,而备注描述的移除,审核员用一条命令就能验证。
审核备注到底是干什么用的
经过四轮提交和一次申诉,总体轮廓是这样的:
它们没法让功能变得可发现。 如果审核员需要被告知按钮在哪里,那按钮就放错了地方,备注只是在给一个你的用户也会遇到的 bug 打补丁。我的就是。
它们赢不了一场关于准则的辩论。 2.4.5 管的是权限的用途。我的备注准确而详尽地解释了一种用途,而准则不允许这种用途。
它们特别擅长让一次移除变得可验证。 “功能已经没了”是一句声明。“nm -u 对这三个符号一无所获”是一个检查,审核员十秒钟就能做完。
它们擅长预防一个还没人问过的问题。 关于试用的那段可能什么都没做。它只花了我四句话。
它们会过期,而最好的段落过期最快。 这一点,我是在写完本文其余部分四天后才学到的,就是下面最后一节。
还有最大的一条:审核备注是拿着特定设备的人读的。 我的备注描述了一个那台设备没有的布局。在你写下“在侧边栏里”或“在右上角”之前,请在被拒信息点名的设备类别上打开你的 App,或者在最可能被用来审核的设备上打开,然后描述那个屏幕上真实存在的东西。
Fingertips 现在三个平台都在 App Store 上了。从首次提交到首次过审 24 天,最后一轮只用了 1 天。我能指出的区别,不是一段更好的说辞,而是一个不需要说辞的构建。
四天后,我删掉了其中三分之一
上面这些,都是我 9 月 6 日写的。9 月 10 日,我在两个平台提交了 1.2.0,打开审核备注,本来只想给一个新功能加一段说明,结果把它们整个重写了。
自动粘贴那一节最先被删。 整节全删:960 个字符,**占过审的 macOS 备注的 36%**,标题是 _请先阅读_,上文我把它当成整份备注里最有力的内容来引用。删掉它,还迫使我修正了这篇文章前面的一处断言。
我说过,在第一行承认被拒是对的,因为审核员反正能看到历史。这在同一版本内是对的。那份备注随 1.0 的记录一起回去,进入同一条调解中心(Resolution Center)会话,而那条被拒记录就在这个会话里。1.2.0 是全新版本,会话是空的。 同一段文字,9 月 5 日读起来像一份状态报告,9 月 10 日读起来就像在为一件已经了结的案子辩解,而且一个字都没改。那种观感从来不属于文字本身。它属于提交本身。
然后是所有构建编号。 过审的 iOS 备注写着_“这是 build 22 做错的地方”_和_“在 build 24 中,iPhone 上……”_,可实际附带的构建是 25。Mac 那份我在提交前发现了,手动改了过来。iOS 那份发出去时指向的构建编号,根本不是正在审核的那个,结果还是过审了,没人追问。1.2.0 的备注里一个构建编号都没有,只有这种写法才不会腐烂。
粘贴材料里有两句活了下来,因为它们仍然在回答一个关于 Mac 剪贴板工具的实时问题:_这东西会替我粘贴吗,它被允许碰什么?_ 它们不再是一份供词,而变成了一段描述:
App 不注入任何合成事件,不做任何辅助功能 API 调用,也不请求辅助功能权限。这一点可以验证,不是空口声明:对已发布的可执行文件运行 "nm -u",针对 AX、CGEvent 或 PostEvent 一无所获。960 个字符变成 242 个。同一事实,同一检查,不再为一个已经了结的案子辩护。
接下来是我没预料到的部分:备注并没有变短。
| 3,431 | ||
| 2,644 |
macOS 最终只差了 14 个字符,而里面超过三分之一的内容都被替换掉了。iOS 多了一千个字符,因为 1.2.0 增加了一行,显示 CloudKit 用户记录名的 8 个字符,用来让用户分辨自己的两台设备是不是同一个 iCloud 账号。它看起来正是审核员会标记的那类标识符,所以备注现在一开头就说明它是什么、不是什么、为什么存在。提前预防没有停止。它转移到了真正新的东西上。
这就是我写这篇文章前半部分时还没有的教训。审核备注不是一份你可以持续维护的文档。它是一次快照,拍的是你预期某一位审核员会问什么:他手里拿着某一个构建,面前是某一条版本记录。 这三者任何一个变了,它就得重写。最好的段落过期最快,因为一段文字越擅长回答问题,就越紧地绑在那个问题还活着的时刻上。
每次提交前重读它们,就当上一次被拒从来没有发生过。对打开它们的人来说,确实没有发生过。
Fingertips 是一款面向 iPhone、iPad 和 Mac 的代码片段库,为 RevenueCat 2026 年 Shipaton 黑客松打造。它已上架 App Store,官网是 usefingertips.com。本系列此前还有:一个把不可达的 RevenueCat 变成无限许可证的 entitlement 检查;为什么不能在提交前给黑客松评委一个免费解锁;一个无视搜索工具、凭空编造数据的端上模型;以及那次因为购买按钮在 iPhone 布局上根本不会被渲染而被拒的经过。
来源
原文标题:The Best Paragraph in My App Review Notes Was the First One I Deleted 作者:Peter Dongo 发布平台:HackerNoon 原文链接:https://hackernoon.com/the-best-paragraph-in-my-app-review-notes-was-the-first-one-i-deleted
相关链接
Fingertips App Store 页面:https://apps.apple.com/app/id6798439217 Fingertips 官网:https://usefingertips.com/ 一个把不可达的 RevenueCat 变成无限许可证的 entitlement 检查:https://hackernoon.com/one-early-return-turned-my-14-day-trial-into-a-free-licence 为什么不能在提交前给黑客松评委免费解锁:https://hackernoon.com/can-you-give-a-hackathon-judge-a-free-unlock-before-you-submit-no-and-here-is-the-exact-409 一个无视搜索工具、凭空编造数据的端上模型:https://hackernoon.com/i-gave-an-on-device-llm-a-search-tool-it-ignored-it-and-made-up-data-instead 苹果因为购买按钮太难找到而拒绝了我的 App:https://hackernoon.com/apple-rejected-my-app-because-its-buy-button-was-too-hard-to-find
相关阅读:
一个运行了一年的个人 AI Agent:自动整理日历、邮件、会议和工作记忆
