iOS 原生开发的平台细节:Swift 6 与严格并发、SwiftUI、Xcode 与最低部署版本、Swift Package Manager、格式与 lint、ATS 与 Keychain、隐私清单与权限、后台任务与扩展、测试、签名与分发。
新 target 使用 Swift 6 语言模式;仍停在 Swift 5 模式的老 target 要有迁移计划和截止日期
怎么检查:用 xcodebuild -showBuildSettings 检查每个 target 的 SWIFT_VERSION;例外清单里登记 target、原因和截止日期
为什么、来源
为什么:Swift 6 语言模式把数据竞争从运行时偶发崩溃变成编译错误;长期停在旧模式,迁移债只会越积越多。
- 老项目按模块逐个迁移,先在 Swift 5 模式下打开完整并发检查消掉警告,再切到 Swift 6 模式。
- 不要用全仓
@preconcurrency import 一次性压掉所有警告来「完成迁移」。
每个 target 显式设置默认 actor 隔离和「易用并发」相关的构建设置,不依赖 Xcode 模板的默认值
怎么检查:把各 target 的 SWIFT_DEFAULT_ACTOR_ISOLATION、SWIFT_APPROACHABLE_CONCURRENCY 导出成快照,CI 比对变化,变化时要求评审确认
为什么、来源
为什么:默认隔离设成 MainActor 与否,会改变同一段代码的隔离语义;不同 target 默认值不同时,搬代码就会出现难查的行为变化。
- App 和界面相关的 target 常设为 MainActor 默认隔离;纯逻辑库、网络库保持非隔离(nonisolated),需要时再显式标注。
更新界面状态的代码放在主 actor(@MainActor);跨任务共享的可变状态用 actor 或不可变值类型保护
怎么检查:在 Swift 6 语言模式下编译不报并发错误;评审时检查共享可变状态的保护方式,必要时用 Thread Sanitizer 跑测试
为什么、来源
为什么:UIKit 和 SwiftUI 只能在主线程更新;没有保护的共享可变状态就是数据竞争。
不用 @unchecked Sendable、nonisolated(unsafe) 压制并发错误;确需使用时就地注释理由,并登记到例外清单
怎么检查:SwiftLint 自定义规则或 CI 里的 grep 扫描这些关键字,与例外清单比对,不在清单里就失败
为什么、来源
为什么:这两个标注是告诉编译器「相信我」,用来消警告时会把真正的数据竞争藏起来。
- 先改成值类型、actor 或明确的隔离边界;只有包装了自带锁的老代码等确有理由的情况才保留。
生产代码不用 try! 和强制解包(!);测试代码和确定不会失败的字面量除外
怎么检查:在 SwiftLint 打开 force_try、force_unwrapping 规则,测试 target 单独放宽
为什么、来源
为什么:一旦遇到意外数据就直接崩溃,而且崩溃点往往离真正的原因很远。
上传到 App Store Connect 的构建使用满足 App Store 当前要求的 Xcode 和 SDK;CI 固定 Xcode 版本,不用 beta 版构建发布包
怎么检查:CI 开头打印并断言 xcodebuild -version;发布流水线拒绝 beta 版 Xcode;每次 Apple 更新上传要求时同步更新 CI 镜像
参考值:核实于 2026-11-14(Apple「Upcoming Requirements」页面):自 2026-04-28 起,上传的 App 必须用 Xcode 26 及以后、iOS 26 SDK 及以后构建;自 2026-09-09 起,上传的 iOS/iPadOS App 最低部署版本不得低于 iOS 13。Apple 通常每年春季提高一次 SDK 要求,以官方页面为准。
为什么、来源
为什么:不满足上传要求的构建会被 App Store Connect 拒收;beta 工具链的行为可能和正式版不同。
- 升级 Xcode 或 Swift 时,先在分支上跑完整的编译、测试和快照回归,再更新 CI 里固定的版本。
最低部署版本按目标用户的设备数据决定,写进决策记录,并列出因此不能用或要降级的系统 API
怎么检查:CI 比对各 target 的 IPHONEOS_DEPLOYMENT_TARGET 与决策记录里的值;评审时确认决策记录有设备数据依据
参考值:参考:Observation 和 SwiftData 需要 iOS 17 及以后,最低部署版本低于 iOS 17 时就要保留 ObservableObject、Core Data 等旧方案。公开统计(App Store 使用统计)只代表 App Store 全体设备,企业内部设备要用客户自己的数据。
为什么、来源
为什么:编译用的 SDK、最低部署版本、用户实际系统版本是三件事。定得太高会丢用户,定得太低则用不上 Observation、SwiftData 等新接口。
- 高于最低版本的 API 用
if #available / @available 保护,并写好降级路径。
- 每年新系统发布后复核一次。
新界面默认用 SwiftUI;富文本编辑、相机与视频、复杂列表性能等 SwiftUI 不擅长的场景用 UIKit,并通过 UIViewRepresentable / UIViewControllerRepresentable 互操作
怎么检查:评审新界面时确认使用 UIKit 的部分写明了原因
为什么、来源
为什么:SwiftUI 是 Apple 主推的界面框架,新特性先到 SwiftUI;但某些场景 UIKit 仍更成熟,硬套 SwiftUI 会写出大量绕路代码。
最低部署版本 iOS 17 及以后的新代码用 Observation(@Observable)管理状态;与 ObservableObject 并存时在模块边界隔开,不在同一层混用
怎么检查:扫描同一模块里同时出现 @Observable 与 ObservableObject 的情况,提示评审确认
为什么、来源
为什么:Observation 只在读到的属性变化时刷新视图,比 ObservableObject 更精细;两套机制混用时很难判断刷新来自哪里。
- 状态对象由根视图或组合根持有(
@State),不在 body 里反复创建。
View.body 里不发网络请求、不读写数据库、不创建重量级对象;副作用放到 .task 修饰符或模型层
怎么检查:评审时检查 body 内是否调用了网络、数据库、文件接口;可用 SwiftLint 自定义规则找候选
为什么、来源
为什么:body 会被系统频繁、不可预测地重新计算,里面的副作用会被重复执行,也很难测试。
架构从「功能模块 + 观察模型 + Repository + 协议注入」起步;引入 The Composable Architecture(TCA)等状态框架要写决策记录证明必要
怎么检查:评审新增的架构类依赖时查看对应的决策记录
为什么、来源
为什么:小中型 App 用系统能力就够;重型架构框架学习和升级成本高,换掉也难。
依赖用 Swift Package Manager 管理;新项目不引入 CocoaPods 和 Carthage,只有不提供 SPM 的厂商 SDK 才用 XCFramework 接入
怎么检查:CI 检查仓库里没有新增的 Podfile、Cartfile
为什么、来源
为什么:SPM 内置在 Xcode 和 Swift 工具链里,不需要额外工具;社区新库普遍优先支持 SPM。
App 仓库把 Package.resolved 提交入库;CI 只按锁文件解析依赖,不自动升级
怎么检查:CI 检查 Package.resolved 存在;构建时加 -disableAutomaticPackageResolution(或 -onlyUsePackageVersionsFromResolvedFile),锁文件与解析结果不一致就失败
为什么、来源
为什么:不锁版本,同一个提交在不同时间构建会拿到不同的依赖,问题无法复现。
- 供别人引用的 Swift 包(库)对依赖写版本范围(
from:、.upToNextMinor),不要 exact: 写死,否则容易和使用方的其他依赖解析冲突;由使用方的 App 锁定具体版本。
- 要求断网构建时,依赖源和二进制工件要有内部镜像,见 28。
新增第三方运行时依赖前写明用途、许可证、维护状况、传递依赖和换掉它的办法;系统框架能满足时不引入
怎么检查:CI 发现 Package.resolved 新增包时,要求 PR 描述里有这些信息
为什么、来源
为什么:每个依赖都会带来升级、许可证、隐私清单和体积的长期成本;一个只做展示和简单接口调用的 App 完全可以零第三方运行时依赖。
用 swift-format 管格式、SwiftLint 管质量规则;关掉 SwiftLint 里与格式重叠的规则,CI 用严格模式运行二者
怎么检查:CI 运行 swift-format lint --strict --recursive <源码目录> 和 swiftlint lint --strict;配置文件 .swift-format、.swiftlint.yml 入库
为什么、来源
为什么:两个工具都管缩进、行宽时会互相打架,开发者改了一个又被另一个报错。
- swift-format 已随 Xcode 工具链提供,可用
xcrun swift-format 调用;团队需要和 CI 一致时固定版本。
- 只想用一个工具也可以,但规则必须可复现,不能靠人工风格评审。
网络请求默认用 URLSession + async/await + Codable;设置超时、响应任务取消,按状态码分类错误,日志里不打令牌和个人信息
怎么检查:网络层单元测试用 URLProtocol 桩覆盖超时、取消、4xx、5xx、解析失败;评审日志语句
为什么、来源
为什么:系统网络库已覆盖常见需求;不处理取消和超时会让界面卡住,所有错误都显示「网络错误」会掩盖权限过期等真实原因。
不在 Info.plist 里全局关闭 App Transport Security(NSAllowsArbitraryLoads);必须用明文 HTTP 的,在 NSExceptionDomains 里逐个限定域名并写明理由
怎么检查:CI 扫描所有 target 的 Info.plist,出现 NSAllowsArbitraryLoads = true 即失败;NSExceptionDomains 的每一项要在例外清单里
为什么、来源
为什么:ATS 默认要求 HTTPS,全局关闭等于让所有请求都可能被中间人窃听和篡改;App 审核也会要求说明理由。
- 内网自签证书的正确做法是通过 MDM 下发企业根证书,而不是关掉 ATS。
访问令牌、刷新令牌、密码、私钥只存 Keychain,并选择合适的可访问性等级;不存 UserDefaults、plist 或普通文件
怎么检查:扫描代码里写入 UserDefaults、@AppStorage 的键名是否含 token、password、secret 等字样;评审 Keychain 封装的可访问性设置
为什么、来源
为什么:UserDefaults 和普通文件是明文存储,会进入备份,越狱设备上可直接读取。
- 不需要跨设备迁移的凭据用
kSecAttrAccessibleWhenUnlockedThisDeviceOnly 或 AfterFirstUnlockThisDeviceOnly(后台要访问时)。
- 要和扩展共享时用 Keychain 访问组,只共享必需的条目。
- 优先用成熟的 Keychain 封装或自己写一层很薄的封装,并有单元测试。
存放敏感业务数据的文件设置数据保护等级(如 FileProtectionType.complete),退出登录时删除
怎么检查:评审写文件的代码是否指定了保护等级;真机测试锁屏后读不到、退出登录后文件被清除
为什么、来源
为什么:数据保护让文件在设备锁定时保持加密;退出登录不清理,下一个使用者就能看到上一个人的数据。
App 和每个扩展都包含隐私清单(PrivacyInfo.xcprivacy),按实际使用声明收集的数据类型和必需原因 API 的理由,不复制模板
怎么检查:归档后用 Xcode 的「Generate Privacy Report」汇总所有隐私清单;扫描代码里的必需原因 API(如 UserDefaults、文件时间戳、系统启动时间、磁盘空间),确认每类都有对应的理由代码
为什么、来源
为什么:自 2024-05-01 起,用到必需原因 API 却没有声明理由的 App(包括通过第三方 SDK 用到的)会被 App Store Connect 拒收;照抄模板会让声明和真实行为不符。
引入 Apple 列名的常用第三方 SDK 时,确认它自带隐私清单;以二进制形式引入的还要确认带签名
怎么检查:新增或升级依赖时对照 Apple 的第三方 SDK 名单检查;Xcode 会校验新版本 SDK 与旧版本签名者一致
为什么、来源
为什么:提交包含名单内 SDK 的新 App 或新增这类 SDK 的更新时,Apple 要求必须有隐私清单,二进制依赖还需签名。
- 名单包括 Alamofire、Kingfisher、SDWebImage、Lottie、Firebase 各组件、Flutter 及其插件等,以官方页面为准;重新打包这些 SDK 的产物同样适用。
App Store 的 App 隐私详情与隐私清单、第三方 SDK 的实际采集保持一致;增删或升级 SDK 后重新核对
怎么检查:发布前对照 Privacy Report、SDK 清单和 App Store Connect 里的隐私详情,逐项签字确认
为什么、来源
为什么:隐私详情不实违反审核指南,也可能构成对用户的虚假陈述。
只有在做 Apple 定义的「跟踪」(跨其他公司的 App 或网站关联用户数据)时才用 App 跟踪透明度(ATT)请求授权;用户拒绝后不用其他标识继续跟踪
怎么检查:评审时对照 SDK 清单判断是否构成跟踪;测试拒绝授权后广告标识为全零、没有用指纹等方式替代
为什么、来源
为什么:未授权跟踪和用设备指纹绕过 ATT 都违反 App Store 政策;没有跟踪却弹 ATT 只会打扰用户。
Info.plist 里每个权限用途文案(NS…UsageDescription)写清 App 用它做什么,并本地化;在用户用到对应功能时再发起授权请求
怎么检查:CI 检查代码用到的权限都有对应的用途键、文案非空且不是占位语;评审授权请求的触发时机
为什么、来源
为什么:缺少用途键的 App 一调用权限接口就会崩溃;文案含糊或启动时一次弹出所有权限,会被审核拒绝,用户也更容易拒绝。
支持创建账号的 App 在 App 内提供删除账号的入口
怎么检查:发布检查清单里确认删除入口可达、能真正删除账号及关联数据
为什么、来源
为什么:App 审核指南要求支持账号创建的 App 必须允许用户在 App 内发起删除账号。
后台工作用 BGTaskScheduler,设置过期处理(expirationHandler)、持久化进度、能中断后重入;不把后台任务当定时器
怎么检查:代码检查每个任务都设置了 expirationHandler 并调用 setTaskCompleted;真机上用调试命令触发任务和过期,验证中断后能恢复
为什么、来源
为什么:后台任务何时运行、能跑多久都由系统决定,随时可能被终止;按「每 N 分钟执行一次」设计的功能在 iOS 上不成立。
- 大文件上传下载用后台 URLSession(
URLSessionConfiguration.background),App 被挂起或结束后系统继续传输。
BGContinuedProcessingTask 只用于用户在前台主动发起、进入后台后继续的任务,要展示进度、允许取消,不是常驻后台的豁免。
- 需要长时间持续计算的工作放到服务端。
推送的设备令牌每次启动都重新上报服务端,并标明开发或生产环境;推送只用来提醒,业务数据以服务端为准
怎么检查:测试令牌变化、切换环境、丢失推送三种情况,确认服务端映射正确、打开 App 后能补齐数据
为什么、来源
为什么:APNs 令牌会在重装、恢复备份等情况下变化,开发和生产环境的令牌不通用;推送本身不保证送达。
APNs 认证密钥(.p8)只放在服务端的密钥库,不打进 App、不进仓库
怎么检查:密钥扫描规则匹配 .p8 文件和 -----BEGIN PRIVATE KEY-----;App 包内容扫描
为什么、来源
为什么:拿到 .p8 密钥就能以你的身份给所有用户发推送。
通知服务扩展只做轻量处理(如下载附件、解密正文),在系统时限内完成;时限将到时回退显示原始内容
怎么检查:检查实现了 serviceExtensionTimeWillExpire;在慢网和断网下测试通知仍能显示
为什么、来源
为什么:扩展有严格的时间和内存限制,超时不处理会导致通知内容异常。
小组件按时间线和刷新预算设计,界面能接受延迟刷新;不做秒级实时看板
怎么检查:评审小组件的刷新策略;锁屏可见的小组件确认不显示敏感数据
为什么、来源
为什么:小组件不是常驻进程,系统按预算刷新,频繁请求刷新只会更快耗尽预算。
每个 target 和扩展的 entitlements 只开必需的能力;App Group 和 Keychain 共享组只共享必需的数据
怎么检查:CI 把各 target 的 .entitlements 导出比对,变化时要求评审说明
为什么、来源
为什么:扩展是独立进程,多开的能力和共享容器扩大了数据暴露面,审核时也要逐项说明。
新的单元和集成测试用 Swift Testing;UI 自动化和性能测试用 XCTest(XCUITest);存量 XCTest 用例不强制重写
怎么检查:评审新增测试文件使用的框架;两套框架可在同一个测试 target 里共存
为什么、来源
为什么:Swift Testing 是 Xcode 自带的新单元测试框架,表达力更好;它不提供 UI 测试接口,UI 测试仍要用 XCUITest。
UI 测试用 accessibilityIdentifier 定位元素,并在关键页面调用 performAccessibilityAudit() 做自动无障碍审计
怎么检查:UI 测试里不出现坐标点击;关键页面的测试包含无障碍审计调用,CI 上审计失败即测试失败
为什么、来源
为什么:按文字或坐标定位的测试随文案和布局变化就断;自动审计能低成本发现缺标签、对比度不足、文字被截断等问题。
CI 保存测试结果包(.xcresult),失败时能看到截图和日志
怎么检查:xcodebuild test 加 -resultBundlePath,CI 把结果包作为构建产物上传
为什么、来源
为什么:只有一行「测试失败」无法排查,结果包里有失败截图、日志和附件。
快照测试只覆盖关键页面,固定模拟器机型、系统版本、语言、字号和深浅色;基线变化必须人工审阅,CI 不自动接受
怎么检查:快照配置写死运行环境;基线图片的改动走 PR 评审
为什么、来源
为什么:系统或 Xcode 升级会改变字体渲染和界面外观;自动接受新基线等于不测。
控件提供 VoiceOver 能读懂的 accessibilityLabel,必要时补 accessibilityValue、accessibilityHint;只有图标的按钮必须有标签,自定义控件声明正确的 traits
怎么检查:XCUITest 的无障碍审计找候选;VoiceOver 真机走一遍核心流程
为什么、来源
为什么:没有标签的按钮在读屏下只会被读成「按钮」或图片名,读屏用户无法使用。
文字使用动态字体的文本样式(SwiftUI 的 .font(.body) 等,UIKit 的 preferredFont(forTextStyle:) 加 adjustsFontForContentSizeCategory),最大辅助字号下核心内容不截断
怎么检查:用最大辅助字号截图核心页面并人工检查;无障碍审计会报告被截断的文字
为什么、来源
为什么:写死字号的文字不跟随系统设置,视力不好的用户放大后要么看不清,要么被截断。
颜色用系统语义色或资产目录里带深浅色变体的颜色;动画遵守「减弱动态效果」设置;支持 iPad 时适配尺寸变化和分屏
怎么检查:用深色、浅色、增强对比度、减弱动态效果、iPad 分屏组合截图检查
为什么、来源
为什么:写死的颜色在深色模式下会出现白底白字;不响应减弱动态效果会让前庭敏感的用户不适;iPad 上直接放大 iPhone 布局体验很差。
- SwiftUI 读
@Environment(\.accessibilityReduceMotion),UIKit 读 UIAccessibility.isReduceMotionEnabled。
界面文案放进 String Catalog(.xcstrings);日期、数字、货币用 FormatStyle 按区域格式化,复数用 String Catalog 的复数变体,不拼接句子
怎么检查:CI 检查 String Catalog 里没有「需要审阅」「未翻译」状态的条目;伪本地化跑一遍 UI 测试
为什么、来源
为什么:拼接的句子换语言后语序错误;自己格式化的日期和数字在其他地区显示不对。
签名证书私钥、描述文件、App Store Connect API 密钥放在密钥库或 CI 机密里,按环境隔离,不进仓库
怎么检查:密钥扫描匹配 .p12、.mobileprovision、.p8 和私钥头;CI 签名凭据只在发布流水线可见
为什么、来源
为什么:签名私钥泄露后,别人能以你的身份签发 App。
每个发布构建保留 .xcarchive、dSYM 符号文件、对应提交编号、Xcode 版本和 Package.resolved
怎么检查:发布流水线把这些产物归档,缺一项就失败;dSYM 同时上传到崩溃上报服务
为什么、来源
为什么:没有符号文件,线上崩溃堆栈无法还原;没有提交和工具链信息,问题版本无法重建。
分发渠道在立项时书面决定并符合 Apple 的资格条件;不用 Enterprise 证书给外部客户或公众分发
怎么检查:评审分发决策记录:用户是谁、用哪个渠道、组织是否符合该渠道的资格
为什么、来源
为什么:Enterprise 计划只允许组织给自己的员工分发内部 App,违规使用会被吊销证书,已安装的 App 全部无法打开。
- 面向公众:App Store,预发布测试用 TestFlight。
- 面向特定的外部企业客户:通过 Apple Business 分发的自定义 App(Custom Apps)。
- 组织内部员工专用、且符合 Enterprise 计划条件:Enterprise 计划 + MDM;先评估自定义 App 是否够用。
- 少量测试设备:Ad Hoc。
- TestFlight、App Store、Apple Business 都依赖 Apple 的在线服务;「离线业务」不等于「离线分发」,客户网络不能访问 Apple 服务时要在立项时说明。
在欧盟通过替代应用市场或网页分发前,确认已签署相应条款、App 能通过苹果公证,并评估佣金
怎么检查:分发决策记录里写明适用条款、公证流程和费用测算
参考值:核实于 2026-11-14:Apple 于 2026-08 宣布新的欧盟条款,替代应用市场和网页分发的 App 改收 5% 的核心技术佣金(Core Technology Commission),取代原来的按安装收取的核心技术费,新条款 2026-10-01 生效;所有替代分发的 App 仍需公证。
为什么、来源
为什么:欧盟《数字市场法》(DMA)下的替代分发仍要经过苹果的公证审查,条款和收费近年变化频繁,按旧资料决策容易出错。
提交审核时提供可用的演示账号和审核期间可访问的后端;需要特殊硬件或环境的功能附演示视频
怎么检查:发布检查清单确认演示账号在提交当天能登录、后端对公网可达
为什么、来源
为什么:审核人员登录不了或连不上服务,App 会直接被拒。
在中国大陆 App Store 上架、且需要备案的 App,在 App Store Connect 填写与 App 一致的 APP 备案号
怎么检查:上架前核对备案主体、App 名称、Bundle ID 与 App Store Connect 里填写的备案号一致
为什么、来源
为什么:缺少或不匹配的备案信息会导致 App 无法在中国大陆 App Store 上架。是否需要备案按 27 的口径判断。
React Native、Flutter 等跨平台项目同样维护 iOS 原生资产:隐私清单(含各插件的清单)、entitlements、权限用途文案、签名配置
怎么检查:归档后用 Privacy Report 检查所有插件的隐私清单都已合并;CI 比对 entitlements 和 Info.plist 变化
为什么、来源
为什么:JS 或 Dart 层的配置不会自动满足 iOS 的权限、隐私和签名要求,审核看的是最终的原生包。
没有符合条件的规则。
工具链
当前正式版 Xcode(满足 App Store 当前上传要求),CI 固定版本
不推荐:beta 版 Xcode 构建发布包;在 Linux 上构建签名 iOS App
界面
SwiftUI
备选:UIKit(通过 UIViewRepresentable 互操作)(富文本编辑、相机视频、复杂列表性能,或维护存量 UIKit 项目)
不推荐:自己造界面框架
状态观察
Observation(@Observable),需要 iOS 17 及以后
备选:ObservableObject / Combine(最低部署版本低于 iOS 17 或存量代码)
不推荐:同一层混用两套观察机制
架构
功能模块 + 观察模型 + Repository + 协议注入(组合根手动注入) · TCA 为 MIT
备选:The Composable Architecture(TCA)(多团队共享复杂状态、副作用多且要求可测试,并有决策记录)
不推荐:小项目强制套重型架构框架
网络
URLSession + async/await + Codable · swift-openapi-generator 为 Apache-2.0;Alamofire 为 MIT
备选:swift-openapi-generator(Apple 官方,配合 URLSession 传输层)(后端有稳定的 OpenAPI 契约且接口较多);Alamofire(确实需要它的拦截、重试等能力且已评估)
不推荐:自研 HTTP 协议栈;手抄大量接口 DTO
本地存储
偏好用 UserDefaults / @AppStorage,凭据用 Keychain,简单对象模型用 SwiftData · GRDB 为 MIT;需要加密时评估 SQLCipher 的许可与出口管制
备选:GRDB.swift(离线优先、需要明确的 SQL、事务和迁移控制);Core Data(存量项目继续使用,不为追新而重写)
不推荐:自己封装 SQLite 和连接池;把 SwiftData 或 GRDB 当成自带同步或加密
图片加载
SwiftUI AsyncImage · Nuke、Kingfisher 均为 MIT
备选:Nuke 或 Kingfisher(需要磁盘缓存、预加载、渐进式解码等高级能力;同一项目只用一个)
依赖管理
Swift Package Manager,Package.resolved 入库
备选:XCFramework(厂商 SDK 不提供 SPM 包)
不推荐:新项目用 CocoaPods 或 Carthage
格式与 lint
swift-format(格式)+ SwiftLint(质量规则) · swift-format 为 Apache-2.0;SwiftLint 为 MIT(均为独立运行的开发工具,不进入 App)
备选:只用其中一个(团队愿意放弃另一类检查,但规则要可复现)
不推荐:两个工具同时管缩进和行宽
测试
Swift Testing(单元、集成)+ XCUITest(UI) · swift-snapshot-testing 为 MIT
备选:XCTest 单元测试(存量用例,或需要 XCTest 独有的性能测试接口);swift-snapshot-testing(设计关键页面需要视觉回归;只放在测试 target)
不推荐:用 Swift Testing 替代 UI 测试
构建与发布自动化
自建 macOS 构建机 + xcodebuild,按需加 fastlane · fastlane 为 MIT
备选:Xcode Cloud(可以接受 Apple 访问源码仓库并在云端构建)
不推荐:签名私钥进 Git、共用个人开发者账号
分发
App Store(预发布测试用 TestFlight)
备选:Apple Business 分发的自定义 App(Custom Apps)(给特定外部企业客户);Enterprise 计划 + MDM(符合资格的组织给自己员工分发内部 App);Ad Hoc(少量登记设备的测试);欧盟替代应用市场或网页分发(面向欧盟用户且已评估条款、公证和佣金)
不推荐:用 Enterprise 证书给外部客户或公众分发
✕ 错误做法:为了消掉 Swift 6 并发错误批量加 @unchecked Sendable
✓ 应该:改成值类型、actor 或明确的隔离边界
✕ 错误做法:到处用 DispatchQueue.main.async 和 Task.detached 修状态问题
✓ 应该:用 @MainActor 和结构化并发,任务随所有者取消
✕ 错误做法:新项目一上来就引入 TCA、Alamofire、图片库、ORM 全家桶
✓ 应该:从 SwiftUI + Observation + URLSession 起步,按需并有理由地加依赖
✕ 错误做法:为了调试方便全局关闭 ATS
✓ 应该:用 HTTPS;内网证书通过 MDM 下发信任,必要时按域名设例外
✕ 错误做法:令牌、手机号、密码存在 UserDefaults
✓ 应该:凭据存 Keychain,敏感文件设数据保护
✕ 错误做法:从别的项目复制 PrivacyInfo.xcprivacy 应付审核
✓ 应该:按真实使用的 API 和 SDK 填写,归档后看 Privacy Report
✕ 错误做法:App 一启动就请求所有权限
✓ 应该:用户用到功能时再请求,被拒后降级
✕ 错误做法:把静默推送或后台任务当可靠的定时调度
✓ 应该:系统调度 + 本地持久化任务 + 进入前台时补做
✕ 错误做法:小组件每隔几秒请求刷新
✓ 应该:按时间线设计,需要实时进度时评估实时活动(Live Activity)
✕ 错误做法:快照测试全量截图并在 CI 自动接受差异
✓ 应该:只测关键页面,固定运行环境,基线变化人工审阅
✕ 错误做法:用 Enterprise 证书签 IPA 发给客户
✓ 应该:用 Apple Business 自定义 App 或 App Store