AI 生成的 iOS 代码能上架吗?上架前检查清单:隐私、权限与审核
AI 生成的 iOS 代码能编译不等于能上架,常出问题的是权限用途说明、隐私清单、高于部署目标的 API、编出来的非公开方法和日志里的用户数据。本文给出按类别整理的上架前检查清单和让 AI 自查的提示词。
AI 生成的 iOS 代码能编译、能跑,不代表能上架。最容易出问题的是 AI 不会主动处理的部分:权限用途说明、隐私清单、高于部署目标的 API、编出来的非公开方法、日志里的用户数据,以及内购、登录、账号删除三条审核规则。做法是用 Release 包按下面的上架前检查清单逐项核对,每项都写了怎么查,文末附一段让 AI 先自查的提示词。规则以 Apple 官方文档和《App Store 审核指南》为准,截至 2026 年 10 月。
这份上架前检查清单怎么用?
- 对 Release 包做检查。Debug 包里有调试开关、测试地址和预览代码,检查结果不算数。
- 按顺序过一遍总表,每项打勾。打不了勾的,跳到下面对应的小节看具体做法。
- 每次让 AI 改完大块功能就复查一次,不要攒到提交前。新加的权限、SDK、网络请求最容易漏。
审核结果由 Apple 决定,这份清单只能帮你排除常见的、可以提前发现的问题。
总表(可打印)
| 类别 | 检查项 | 怎么检查 | 详见 |
|---|---|---|---|
| 隐私与权限 | 每个用到的权限都有用途说明,写清具体场景 | 搜 AVCaptureDevice、PHPhotoLibrary、CLLocationManager 等调用,逐个对照 Info.plist 的 NS…UsageDescription |
178 |
| 隐私与权限 | 拒绝授权后 App 仍能用其他功能 | 真机上对每个授权弹窗点「不允许」,把主流程走一遍 | 176 |
| 隐私与权限 | 必需原因 API 都在隐私清单里声明 | 搜 UserDefaults、creationDate、systemUptime 等关键字,对照 PrivacyInfo.xcprivacy |
182 |
| 隐私与权限 | 跟踪用户前弹 ATT,有 NSUserTrackingUsageDescription |
搜 advertisingIdentifier、requestTrackingAuthorization,查广告和归因 SDK |
176 |
| 隐私与权限 | 用户数据发给第三方 AI 服务前告知并取得同意 | 搜请求 AI 接口的代码,看发送前有没有说明和同意步骤 | 176 |
| 第三方 SDK | Apple 名单上的 SDK 都带隐私清单,二进制形式的带签名 | 解压 ipa 列出 PrivacyInfo.xcprivacy 和 .framework,对照官方名单 |
182 |
| 第三方 SDK | AI 推荐的库真实存在、仍在维护 | 去库的官方仓库看最近的发布和 Issues | 下文 |
| API | 没有高于部署目标的 API | 用最低部署目标编译,看 is only available in iOS xx or newer |
190 |
| API | 已弃用 API 的警告都处理了 | 编译日志搜 deprecated |
下文 |
| API | 没有编出来的方法、没有非公开 API | 编译报 has no member;搜 NSClassFromString、performSelector、valueForKey:@"_ |
下文 |
| 数据 | 新版本能读旧版本留下的数据 | 先装旧版本产生数据,再覆盖安装新版本 | 202 |
| 数据 | 令牌、密码存在 Keychain,不在 UserDefaults 或文件里 | 搜 token、password 附近的 UserDefaults、write(to: |
下文 |
| 数据 | 日志不打印手机号、令牌等隐私 | 搜 print(、NSLog(、privacy: .public |
194 |
| 网络 | 没有整体关闭 ATS,例外只给必要的域名 | Info.plist 搜 NSAllowsArbitraryLoads |
213 |
| 网络 | 源码里没有硬编码的密钥、测试地址 | 搜 sk_、secret、apiKey、http://、192.168.、localhost |
下文 |
| 审核 | 数字内容、会员用 App 内购买(3.1.1) | 列出所有付费入口,逐个判断属于哪一类 | 176、191 |
| 审核 | 有第三方登录时提供等效登录选项(4.8) | 看登录页有几种登录方式 | 201 |
| 审核 | 能注册就能在 App 内删除账号(5.1.1(v)) | 在 App 里找删除入口,走一遍删除流程 | 176 |
| 审核 | 没有占位文字、空页面、指向测试环境的接口(2.1) | 全局搜 TODO、Lorem、test、staging |
176 |
| 构建 | 用 Release 配置、发布证书构建 | 看构建命令和签名信息 | 130 |
| 构建 | 版本号、构建号比上一次大 | 解压 ipa 打开 Info.plist 看两个号 | 185 |
| 构建 | 预览代码、调试开关不在 Release 包里 | 搜 #Preview、#if DEBUG 的范围 |
190 |
| 构建 | 加密出口合规已声明 | Info.plist 里有 ITSAppUsesNonExemptEncryption |
下文 |
| 构建 | 推送等能力在 entitlements 和描述文件里一致 | 打开包里的 embedded.mobileprovision 看权限 |
195、186 |
检查前要准备什么?
下面每一节都会用到这两样。
Release 包。在 VS Code 运行「KXBuild: 生成 App Store ipa」,或者在命令行构建,得到 build\MyApp.ipa。
1kxbuild build .\MyApp.xcodeproj -c Release --export app-store
ipa 是 zip 格式,用系统自带的 tar 解压到一个临时目录。解压后的 Payload\MyApp.app\Info.plist、PrivacyInfo.xcprivacy、embedded.mobileprovision 都能在 VS Code 里双击打开,用 属性列表编辑器查看,二进制格式也能读。
1mkdir .\ipa-check
2tar -xf .\build\MyApp.ipa -C .\ipa-check
源码搜索。搜索范围只放自己的源码目录,跳过 Pods、.build、node_modules。PowerShell 的写法如下,后文只给关键字。
1Get-ChildItem .\MyApp -Recurse -Include *.swift,*.m,*.mm |
2 Select-String -Pattern '关键字1|关键字2' |
3 Select-Object Path, LineNumber, Line
Linux 或 WSL 上用 grep。
1grep -rnE --include='*.swift' --include='*.m' --include='*.mm' '关键字1|关键字2' MyApp
也可以直接用 VS Code 的全局搜索,打开正则开关,在「要排除的文件」里填 Pods,.build,node_modules。
AI 生成的代码在隐私与权限上常漏什么?
用途说明。AI 加了访问相机、相册、定位、麦克风、通讯录的代码,常常不会同时改 Info.plist。缺少用途说明,App 访问时会被系统终止,上传时会收到 ITMS-90683。
搜下面的关键字,每命中一类,就到 Info.plist 里找对应的 NS…UsageDescription 键,并确认文字写的是具体场景,例如「用于扫描快递单号」,而不是「需要使用相机」。键名对照见 Info.plist 权限用途说明。
1AVCaptureDevice|PHPhotoLibrary|PHPickerViewController|CLLocationManager|AVAudioSession|CNContactStore|EKEventStore|CBCentralManager|HKHealthStore
PHPickerViewController 不需要相册权限,命中它说明可以不申请完整相册访问。审核指南 5.1.1(iii) 要求只申请核心功能需要的数据,能用系统选择器就别申请完整权限。
必需原因 API。几乎所有 App 都用 UserDefaults,所以几乎都要有 PrivacyInfo.xcprivacy。关键字和原因码的对照见 隐私清单怎么写。按 Apple 官方说明,自 2024 年 5 月 1 日起,用了这类 API 却没有声明原因的 App 不被 App Store Connect 接受。
1UserDefaults|@AppStorage|creationDate|modificationDate|systemUptime|mach_absolute_time|volumeAvailableCapacity|activeInputModes
ATT。搜 advertisingIdentifier 和 requestTrackingAuthorization。代码或广告、归因 SDK 要跟踪用户,就要先调用 requestTrackingAuthorization,并在 Info.plist 里写 NSUserTrackingUsageDescription。App Store Connect 的「App 隐私」问卷要和实际行为一致。
发给 AI 服务的数据。审核指南 5.1.2(i) 要求,把个人数据分享给第三方(包括第三方 AI)前要明确告知并取得同意。App 里有「把聊天内容、照片发给大模型」这类功能的,确认发送前有说明和同意步骤,隐私政策里也写上。
AI 推荐的第三方 SDK 要查什么?
Apple 维护一份常用第三方 SDK 名单。新 App 包含名单上的 SDK,或者更新版本新加入名单上的 SDK,要带上它的隐私清单;以二进制形式引入的,还要带签名。名单里有 Alamofire、Firebase 系列、Kingfisher、SDWebImage、Lottie、hermes、Flutter 等常见库。
在解压目录里列出所有框架和隐私清单,对照名单看缺哪个。
1Get-ChildItem .\ipa-check -Recurse -Filter PrivacyInfo.xcprivacy | Select-Object FullName
2Get-ChildItem .\ipa-check\Payload\MyApp.app\Frameworks -Directory | Select-Object Name
AI 推荐库时有两个常见问题:推荐一个已经多年不更新、没有隐私清单的旧版本,或者给出一个根本不存在的包名。加依赖前去库的官方仓库确认三件事:包名和仓库地址对得上,最近一年有发布,最新版本带 PrivacyInfo.xcprivacy。
怎么查高于部署目标的 API 和非公开 API?
部署目标与已弃用 API
AI 默认按最新的 iOS 写代码。工程的最低部署目标低于某个 API 的要求时,编译直接报错。下面是用 iOS 16 作部署目标编译一段 AI 生成代码的实际输出。
1error: 'ContentUnavailableView' is only available in iOS 17.0 or newer
2warning: 'setStatusBarHidden(_:with:)' was deprecated in iOS 9.0: Use -[UIViewController prefersStatusBarHidden]
3error: value of type 'UIView' has no member 'applyBlurEffectAutomatically'
三行分别对应三类问题。第一类用 if #available(iOS 17, *) 包起来,并给低版本写替代界面,写法见 用 AI 写 SwiftUI 的常见坑。第二类按警告里的提示换成新 API,弃用警告现在能编过,但以后的 SDK 可能直接删掉。第三类是 AI 编出来的方法,下一节说怎么处理。
先确认工程的部署目标是多少,再看编译日志。命令行可以导出构建设置查看。
1kxbuild project settings dump .\MyApp.xcodeproj | findstr IPHONEOS_DEPLOYMENT_TARGET
截至 2026 年 10 月,Apple 的上传要求是:自 2026 年 4 月 28 日起,上传的 App 要用 iOS 26 SDK 构建;自 2026 年 9 月 9 日起,iOS App 的部署目标要是 iOS 13 或更高。部署目标具体定多少,见 最低支持 iOS 版本怎么定。
编出来的方法与非公开 API
AI 有时会写出看起来很合理、但 SDK 里根本没有的方法。Swift 代码里这类调用编译不过,报 has no member,这是最可靠的核验方式。遇到这类报错,不要让 AI 「换个写法让它编过」,先去 Apple 开发者文档按类名搜一下,确认有没有实现这个功能的公开 API。
更危险的是能编过的写法。用字符串动态调用的方法,编译器不检查它是否存在、是否公开,审核指南 2.5.1 要求只用公开 API。搜下面这些关键字,逐个确认调用的名字都能在官方文档里查到。
1NSClassFromString|NSSelectorFromString|performSelector|value\(forKey: *"_|valueForKey:@"_|setValue\(.*forKey: *"_|dlopen|dlsym
以下划线开头的键名和类名基本都是非公开的。审核被拒信里通常会列出具体的符号名,按符号名搜自己的代码和依赖,见 审核被拒的 12 个原因。
数据存储和日志要注意什么?
旧数据兼容。AI 改数据模型时只看当前代码,不知道用户手机上还存着旧版本写的数据。给 Codable 结构体加了一个非可选字段,旧的 JSON 里没有这个键,解码就会失败,用户的设置或缓存全部丢失。新字段用 decodeIfPresent 给默认值。
1struct UserSettings: Codable {
2 var fontSize: Int
3 var darkMode: Bool
4 var language: String
5
6 enum CodingKeys: String, CodingKey { case fontSize, darkMode, language }
7
8 init(from decoder: Decoder) throws {
9 let c = try decoder.container(keyedBy: CodingKeys.self)
10 fontSize = try c.decode(Int.self, forKey: .fontSize)
11 darkMode = try c.decode(Bool.self, forKey: .darkMode)
12 language = try c.decodeIfPresent(String.self, forKey: .language) ?? "zh-Hans"
13 }
14}
检查方法是在真机上先装上一个正式版本,用一阵产生数据,再直接覆盖安装新版本,不要先删 App。还要留意 UserDefaults 的键名有没有被 AI 改掉。用 SwiftData 的,模型变化要写迁移,见 SwiftData 本地数据持久化教程。
敏感信息放 Keychain。AI 很喜欢把登录令牌写进 UserDefaults,因为写起来最短。UserDefaults 是明文的 plist 文件,令牌、密码、密钥要放 Keychain。
1import Foundation
2import Security
3
4enum TokenStore {
5 private static let service = "com.example.myapp.auth"
6 private static let account = "accessToken"
7
8 static func save(_ token: String) -> Bool {
9 let base: [String: Any] = [
10 kSecClass as String: kSecClassGenericPassword,
11 kSecAttrService as String: service,
12 kSecAttrAccount as String: account,
13 ]
14 SecItemDelete(base as CFDictionary)
15 var item = base
16 item[kSecValueData as String] = Data(token.utf8)
17 item[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
18 return SecItemAdd(item as CFDictionary, nil) == errSecSuccess
19 }
20
21 static func load() -> String? {
22 let query: [String: Any] = [
23 kSecClass as String: kSecClassGenericPassword,
24 kSecAttrService as String: service,
25 kSecAttrAccount as String: account,
26 kSecReturnData as String: true,
27 kSecMatchLimit as String: kSecMatchLimitOne,
28 ]
29 var result: AnyObject?
30 guard SecItemCopyMatching(query as CFDictionary, &result) == errSecSuccess,
31 let data = result as? Data else { return nil }
32 return String(decoding: data, as: UTF8.self)
33 }
34}
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly 表示开机首次解锁后可读,且不会迁移到其他设备。搜 token|password|secret 附近有没有 UserDefaults、@AppStorage、write(to:,有就改成上面的写法。
日志不打印隐私。AI 调试时会到处加 print,把请求参数、手机号、令牌原样打出来。上架前搜 print(、NSLog(,删掉调试输出,要保留的日志改用 Logger。Logger 插值里的字符串默认隐去,只有你确认不敏感的内容才加 privacy: .public。
1import os
2
3private let authLog = Logger(subsystem: "com.example.myapp", category: "auth")
4
5func logLogin(userID: String, phone: String, success: Bool) {
6 authLog.notice("登录结果 \(success) 用户 \(userID, privacy: .private(mask: .hash))")
7 authLog.debug("手机号 \(phone, privacy: .private)")
8}
再搜一遍 privacy: .public,看后面跟的是不是账号、手机号、令牌这类值。Logger 的级别和隐私选项见 iOS 真机日志怎么看。
网络配置和硬编码密钥怎么查?
ATS。AI 遇到 http 请求失败,最常给的办法是在 Info.plist 里加 NSAllowsArbitraryLoads = YES,整个 App 都不再受 ATS 保护。按 Apple 文档,这类例外提交审核时要说明理由,可能触发额外审核。
打开解压出来的 Info.plist,看 NSAppTransportSecurity 下有什么:能把服务器改成 https 就删掉例外;只有个别域名改不了,就只给它们开 NSExceptionDomains。写法见 iOS 网络请求调试。
硬编码的密钥和地址。AI 生成的示例常常直接把 API Key 写在代码里。打进 App 的字符串谁都能从包里读出来,第三方服务的私钥要放在你的服务端,App 只调用你自己的接口。搜下面的关键字,源码和解压后的 App 都搜一遍。
1sk_|secret|apiKey|api_key|Bearer |PRIVATE KEY|http://|192\.168\.|localhost|staging|test\.
在解压目录里也可以直接搜可执行文件,确认字符串没有残留。
1grep -a -o -E 'sk_[A-Za-z0-9]{8,}|192\.168\.[0-9.]+|http://[A-Za-z0-9.-]+' ipa-check/Payload/MyApp.app/MyApp | sort -u
哪几条审核规则 AI 不会主动考虑?
这三条规则 AI 基本不会主动考虑,要你自己对照。条款原文见 App Store 审核指南,被拒信的读法和回复要点见 审核被拒的 12 个原因。
| 条款 | 要求 | 怎么检查 |
|---|---|---|
| 3.1.1 内购 | 解锁 App 内的功能、会员、虚拟币、付费内容要用 App 内购买,不能用授权码等自有方式解锁 | 列出所有付费入口。数字内容走 StoreKit;实物商品和 App 外使用的服务按 3.1.3(e) 用其他支付方式 |
| 4.8 登录服务 | 用第三方或社交登录作主账号的 App,还要提供一种只收集姓名和邮箱、允许隐藏邮箱、不为广告收集交互数据的登录方式 | 登录页只有微信、Google 这类登录时,加上通过 Apple 登录 |
| 5.1.1(v) 账号删除 | 支持创建账号的 App 必须在 App 内提供删除账号;和账号无关的功能要允许不登录使用 | 在设置里找到删除入口,确认删除的是账号和关联数据,而不只是停用 |
AI 写支付功能时,常会直接接入网页支付或第三方支付 SDK。这在售卖数字内容时会触发 3.1.1。接 App 内购买的完整步骤见 StoreKit 2 接入应用内购买,通过 Apple 登录见 接入「通过 Apple 登录」,它还讲了删除账号时怎么撤销令牌。
AI 生成的界面常带占位内容。全局搜 TODO|FIXME|Lorem|示例|测试数据,审核指南 2.1 要求提交的是最终版本。
上架构建前最后确认哪些项?
- Release 与发布证书。上架包用 Release 配置和 Apple Distribution 证书、App Store 类型的描述文件构建,流程见 在 Windows 上上架 App Store。
- 版本号与构建号。打开解压出来的 Info.plist,看
CFBundleShortVersionString和CFBundleVersion,构建号要比上一次上传的大,规则见 版本号与构建号。 - 调试代码。搜
#if DEBUG,确认测试地址、调试菜单、跳过登录的开关都在里面;#Preview里的示例数据不要被正式代码引用,见 用 AI 写 SwiftUI 的常见坑。 - 加密出口合规。只用 HTTPS 和系统加密的 App,通常在 Info.plist 里把 ITSAppUsesNonExemptEncryption 设为 NO。按 Apple 文档,缺这个键,每次上传新版本 App Store Connect 都会让你回答一遍出口合规问卷。是否属于豁免范围按你实际用的加密判断。
- 能力和描述文件。双击打开解压出来的
embedded.mobileprovision,看权限里有没有你用到的能力,例如推送的aps-environment。AI 加了注册推送的代码却没开能力,上传时会收到 ITMS-90078,推送凭据见 推送 p8 与 p12。上传阶段的报错码见 ITMS 错误码速查表。
怎么让 AI 先自查一遍?
把下面这段连同相关代码、Info.plist、PrivacyInfo.xcprivacy 一起交给 AI,让它先按清单列出问题,你再逐条核实。AI 的回答只是线索,每一条都要用上面的编译和搜索方法确认。
1你是 iOS 上架审核检查员。请检查我提供的代码和配置文件,只列问题,不要直接改代码。
2
3工程信息:
4- 最低部署目标:iOS 16.0
5- 语言与界面:Swift 6,SwiftUI
6- 有账号注册:是;登录方式:手机号、微信
7- 付费内容:会员订阅
8
9逐项检查并按「文件:行号 | 问题 | 依据 | 建议」输出:
101. 每个访问相机、相册、定位、麦克风、通讯录的调用,Info.plist 里是否有对应的 NS…UsageDescription,文字是否写了具体场景。
112. 用到的 UserDefaults、文件时间戳、系统启动时间、磁盘空间、活动键盘 API,PrivacyInfo.xcprivacy 是否声明了对应类别。
123. 是否使用高于 iOS 16.0 的 API 且没有 #available 判断。
134. 是否有你不确定在 Apple SDK 中存在的方法,或用字符串动态调用的方法(NSClassFromString、performSelector、以下划线开头的键)。列出来,标明「需要人工核实」,不要假设它存在。
145. 令牌、密码是否存在 UserDefaults 或文件里;print、NSLog、Logger 是否输出了手机号、令牌等个人信息。
156. Info.plist 是否有 NSAllowsArbitraryLoads;代码里是否有硬编码的密钥、http 地址、内网 IP。
167. 对照 App Store 审核指南 3.1.1、4.8、5.1.1(v):付费方式、登录方式、App 内删除账号是否合规。
178. 是否有 TODO、占位文字、测试环境地址。
18
19没有发现问题的项写「未发现」。不确定的写「不确定」,不要编造 API 名称或审核条款。
工程信息那几行换成你自己的。让 AI 改代码之前,先把它列出的「需要人工核实」项查清楚,再用上面的上架前检查清单把 AI 生成的 iOS 代码完整过一遍。
常见问题
AI 写的 iOS 代码上架前最需要检查什么?
先查四件事:每个权限都有具体的用途说明,PrivacyInfo.xcprivacy 声明了用到的必需原因 API,用最低部署目标编译没有可用性报错,付费、登录、账号删除符合审核指南 3.1.1、4.8 和 5.1.1(v)。这几类问题 AI 最少主动处理。
怎么判断 AI 给的方法是不是真的存在?
先编译,Swift 代码里不存在的方法会直接报 has no member。能编过但用字符串调用的写法(NSClassFromString、performSelector、以下划线开头的键),要去 Apple 开发者文档逐个搜名字,查不到的就当作非公开 API 删掉。
AI 把 NSAllowsArbitraryLoads 设成 YES 能上架吗?
按 Apple 文档,这个例外提交审核时要说明理由,可能触发额外审核。更稳妥的做法是把服务器改成 https 并删掉它;只有个别域名改不了时,只给这些域名配置 NSExceptionDomains。
登录令牌可以存在 UserDefaults 里吗?
不建议。UserDefaults 是明文的 plist 文件,令牌、密码这类敏感信息应该存进 Keychain,并按需要选择 kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly 这类可访问性。
让 AI 自查之后还需要人工检查吗?
需要。AI 的自查结果只能当线索,它可能漏报,也可能编出不存在的规则。每一条都用编译、源码搜索、解压 ipa 和真机测试核实,审核规则以 Apple 官方原文为准。