WWDC Quick Look 💓 By SwiftGGTeam
Build scalable enterprise app suites

Build scalable enterprise app suites

观看原视频

Highlight

Apple Retail 用 App Groups、Swift packages、XCTest、Instruments 和配置驱动 UI 管理多款店内员工 App,让共享数据、共享组件、测试优先级和地区门店配置都能在同一套工程实践下落地。

核心内容

Apple Retail 有 500 多家门店。员工每天使用多款内部 App,处理预约、产品目录、门店布局、客户沟通和运营状态。如果每个 App 都独立处理认证、图片缓存、扫描、模型对象和 UI 组件,工程团队会把大量时间花在重复实现和一致性修复上。

这场 session 给出的做法是把“套件”当成一个工程系统设计。多个 App 仍然保持聚焦,但共享能力被放进明确的边界里:App Groups 负责跨 App 数据,Swift packages 负责复用代码,测试按优先级进入不同频率的执行队列,生产环境差异由配置文件和服务端开关驱动。

这套方法的重点不是某个单一框架。它把企业 App 的四个长期问题放在一起处理:多 App 之间怎样共享,公共代码怎样沉淀,发布前怎样验证,发布后怎样适应不同地区、市场和门店。对企业团队来说,这比做一个庞大的单体 App 更容易维护,也更适合多个业务团队并行演进。

演讲中的扫描示例也很有代表性。Apple Retail 把 Vision 和 AVFoundation 包装成内部的 RetailScanner package,向业务 App 暴露统一的配置对象、默认 UI 和 delegate 回调。业务工程师不需要重复理解相机、条码、OCR 和辅助功能细节,就能在不同 App 里得到一致的扫描体验。

最后,生产配置把业务变化从客户端发版中拆出来。客户表单里的 Email 或 Phone Number 字段要下线时,团队修改托管在服务器上的配置文件,App 下次启动后读取新配置,UI 就能更新。这个机制减少了临时发版,但也要求客户端、服务端和配置版本保持兼容。

详细内容

App Groups:先解决跨 App 共享数据

01:23)Apple Retail 最早采用的 iOS 能力之一是 App Groups。开启后,同一开发者门户下的多个 App 可以通过共享容器交换 UserDefaults 和文件。Xcode 的入口在 Signing and Capabilities 中,添加 App Groups capability 后再创建 group。

01:57)演讲给出的代码从共享 UserDefaults 开始:

let sharedDefaults = UserDefaults(suiteName: "group.com.apple.myappgroup")!

sharedDefaults.set("My Cool Value", forKey: "MyKeyName")

let myKeyNameValue = sharedDefaults.string(forKey: "MyKeyName")

关键点:

  • suiteName 使用 App Group identifier,只有加入同一 group 的 App 才能访问这份 defaults。
  • set(_:forKey:) 写入共享键值,适合放轻量配置和状态。
  • string(forKey:) 读取同一个共享域里的值,其他同组 App 也能按相同 key 取得。
  • transcript 还提到 shared files,适合比键值更大的共享数据。

Swift packages:把重复能力收敛成稳定边界

02:23)Swift packages 在这场演讲里的作用是减少重复代码、降低人为错误,并让工程师跨团队移动时更容易理解已有代码。Apple Retail 还建立了专门维护 packages 的团队,让这些组件有长期负责人。

02:57)演讲列出的候选能力很具体:基于 URLSession 的认证包、图片抓取与磁盘缓存包、UIKit 的自定义 table view cell 和产品详情视图、客户/预约/产品等模型对象,以及扫描包。这些能力有一个共同点:多个 App 都要用,且行为需要一致。

05:03)扫描包的条码示例展示了统一配置对象和 view controller 初始化模式。这里的 RetailScanner 是 Apple Retail 的内部 package,用来说明包装方式,不是公开系统框架。

import RetailScanner

let scanOptions = BarcodeScanOptions()
scanOptions.scanRegion = .regular
scanOptions.shouldAddSupplementaryView = false
scanOptions.shouldShowBarcodeDetector = true

let barcodeViewController = BarcodeScannerViewController(scanOptions: scanOptions)
barcodeViewController.delegate = self

关键点:

  • BarcodeScanOptions 集中保存扫描区域、辅助视图和探测器显示设置。
  • BarcodeScannerViewController(scanOptions:) 把配置作为初始化参数,方便在多个 App 里复用同一套配置。
  • delegate 接收扫描成功和失败消息,业务 App 只处理结果,不重复实现相机和识别流程。

05:23)OCR 示例沿用同样的配置和初始化模式:

import RetailScanner

let scanOptions = OCRScanOptions(
    scanRegion: .custom(CGSize(width: 400, height: 100)),
    accessibilityBehavior: .vibrate,
    shouldAddSupplementaryView: true,
    validation: nil,
    shouldShowResultView: true
)
scanOptions.recognitionLevel = .fast

let ocrViewController = OCRScannerViewController(
    scanOptions: scanOptions
)
ocrViewController.delegate = self

关键点:

  • scanRegion 把 OCR 识别范围限定到自定义尺寸,减少无关画面干扰。
  • accessibilityBehavior 让扫描反馈能照顾辅助功能需求,演讲里提到扫描包支持自定义 accessibility support。
  • recognitionLevel = .fast 对应实时 OCR 场景,优先保证扫描过程中的反馈速度。
  • 条码和 OCR 使用相同设计模式,业务工程师切换能力时不需要重新学习一套 API。

测试:把人工回归拆成可运行的工程流程

05:48)App 做完之后,Apple Retail 没有依赖大量人工测试。演讲把测试分成 unit testing、UI testing 和 performance testing。单元测试由工程团队在写类和函数时补齐,目标是覆盖各个 code path 的期望返回值。

06:40)UI testing 使用 XCUITest,把 QE 团队已有的手工测试用例编码为 Swift。测试创建后还要按重要程度拆分,因为 UI 测试运行时间长,最重要的测试需要更高频执行。

check-in: run the most important UI tests
daily: run a broader set of regression tests
weekly: run lower-priority or longer tests

关键点:

  • check-in 级别测试负责尽快拦截高风险回归。
  • daily 测试扩大覆盖面,适合放核心路径之外的场景。
  • weekly 测试承载耗时更长、优先级较低的回归内容。
  • 按优先级调度能缩短关键失败的发现时间。

07:21)性能测试关注两件事:rendering performance 和 battery performance。门店设备整天在店面使用,员工把设备拿去充电会损失生产力,所以电量表现和滚动、拖拽等交互流畅度一样重要。

Instruments
→ rendering performance
→ memory usage
→ battery usage
→ long-running operations

关键点:

  • Instruments 是 Xcode 内置工具,演讲中用于分析执行时间、内存和电量异常。
  • rendering performance 对应列表滚动、拖拽等高频交互。
  • battery usage 对企业门店场景很关键,因为设备离开店面就会影响员工工作。
  • 这些指标要在真实设备上观察,才能贴近员工全天使用的状态。

配置驱动 UI:让生产差异脱离客户端发版

08:41)生产管理部分讲了两类配置:client based configuration file 和 server based configuration。前者是客户端内有映射,但配置文件托管在服务器上;后者是从服务端下载的布尔值,用来驱动 App 行为。

09:10)客户端配置适合控制 UI 元素。Apple Retail 可以按 geo、market 和 store level 调整体验,不需要改代码或重新提交客户端 build。

PLIST attributes in client code
→ hosted as JSON on the server
→ app downloads attributes during startup
→ UI renders only the configured fields

关键点:

  • PLIST attributes 是客户端已知的字段定义。
  • hosted as JSON 表示配置内容从服务器分发。
  • startup 阶段下载配置,保证 App 打开时使用最新字段。
  • UI 按配置渲染,业务团队可以快速调整门店或市场级差异。

12:10)客户表单示例展示了配置驱动 UI 的落点。官方代码片段只展示了两类 cell 的配置入口:

func configuredCellForLabel(for customerInfoField: CustomerInfoField, at indexPath: IndexPath) -> UITableViewCell { ... }

func configuredCellForPhoneNumber(for customerInfoField: CustomerInfoField, at indexPath: IndexPath) -> UITableViewCell { ... }

关键点:

  • CustomerInfoField 把表单字段抽象成通用输入,便于由配置决定显示哪些字段。
  • configuredCellForLabel 处理普通文本字段,例如 First Name、Last Name 或 Email。
  • configuredCellForPhoneNumber 处理 Phone Number 这类需要特殊输入行为的字段。
  • 当配置文件删除 Email 或 Phone Number,App 下次启动后不再渲染对应 cell。

13:42)配置驱动带来的代价是兼容性管理。新的 server 配置可能旧客户端不认识,新的客户端属性也可能还没有服务端映射。Apple Retail 用 config versioning 判断下载到的 client/server 配置版本,再决定展示哪种体验;遇到双方无法互相支持的情况,就要求用户更新 App。

核心启发

  • 做什么:把认证和网络访问收进共享 package。为什么值得做:transcript 明确提到 Apple Retail 把认证建在 URLSession 之上,业务工程师不用反复处理认证细节。怎么开始:先抽出 request 构造、token 刷新和错误处理,再让各业务 App 依赖同一个 Swift package。

  • 做什么:为条码和 OCR 建一个统一扫描入口。为什么值得做:扫描能力需要 Vision、AVFoundation、默认 UI、辅助功能反馈和 delegate 回调,重复实现容易出现体验差异。怎么开始:照着演讲的 BarcodeScanOptionsOCRScanOptions 和 view controller 初始化模式,先把最常用的扫描流程包成 package。

  • 做什么:把企业 App 的 UI 测试按优先级分层运行。为什么值得做:演讲中的 check-in、daily、weekly 节奏能让关键失败更早暴露,同时保留完整回归覆盖。怎么开始:把现有手工用例转成 XCUITest,再给每条用例标记优先级和运行频率。

  • 做什么:把门店或地区差异放进配置驱动表单。为什么值得做:客户表单示例说明字段下线可以通过配置完成,不需要新的客户端 build。怎么开始:先把表单字段抽象成 CustomerInfoField,再让启动流程下载托管在服务器上的 JSON 配置。

  • 做什么:给配置系统加入版本兼容和强制更新规则。为什么值得做:session 明确指出 server 与 client 配置可能互不支持,缺少版本判断会让旧客户端拿到无法理解的新配置。怎么开始:为 client config 和 server config 分别记录版本,启动时先检查兼容矩阵,再决定启用新体验或提示更新。

关联 Session

评论

GitHub Issues · utterances