为 SwiftUI Previews 优化你的应用架构
Structure your app for SwiftUI previews
2020年6月24日
一句话判断
SwiftUI Previews 不是”锦上添花”的调试工具——如果你的应用架构不支持秒级预览,说明架构本身有问题。这场 Session 教你如何重新组织代码让 Previews 真正发挥威力。
这场 Session 讲了什么
SwiftUI Previews 在 Xcode 12 中变得更加强大,但很多开发者发现自己庞大的项目中 Previews 总是超时或崩溃。Session 的核心论点是:Previews 的可用性直接反映了你的代码架构质量。如果你的 View 依赖了沉重的数据层、单例管理器或者复杂的初始化链,Preview 自然跑不起来。
Session 提出了一套围绕”依赖注入”和”协议抽象”的架构思路。核心思想是把 View 的数据依赖通过构造函数注入,而不是在 View 内部直接访问全局状态或单例。这样在 Preview 中你可以用轻量的 mock 数据替换真实的数据源,让 Preview 在毫秒级完成渲染。具体来说,就是把网络请求、数据库访问、系统服务等全部抽象为协议,然后通过 Environment 或构造参数注入。
此外,Session 还介绍了 Xcode 12 中 Previews 的新能力:支持在 Preview 中运行完整的 SwiftUI App(利用新的 App 协议),支持多个 Preview 同时显示不同状态,以及 Preview 崩溃后的更好诊断信息。
值得深挖的点
PreviewProvider 与静态预览的局限
PreviewProvider 本质上是一个编译时确定的静态提供者。这意味着你无法在运行时动态注入数据,所有 preview 数据必须在编译期就准备好。理解这一点后,你会发现架构中的”可预览性”本质上就是”可测试性”——如果你的 View 可以用 mock 数据快速渲染,那它一定也可以用 mock 数据快速测试。
Environment vs 构造函数注入的选择
Session 比较了两种依赖注入方式。构造函数注入更明确、更适合必需依赖;Environment 更适合跨层级传递的可选依赖。对于 Preview 场景,构造函数注入通常更方便,因为你可以在创建 View 时直接传入 mock 对象。但如果你的 View 嵌套很深,逐层传递构造参数会很痛苦,这时候 Environment 的隐式传递更有优势。
代码片段
// 将数据源抽象为协议,方便在 Preview 中替换
protocol OrderService {
func fetchOrders() -> [Order]
func placeOrder(_ order: Order) -> Bool
}
// 真实实现:从网络获取数据
struct RemoteOrderService: OrderService {
func fetchOrders() -> [Order] {
// 真实的网络请求...
return []
}
func placeOrder(_ order: Order) -> Bool {
// 真实的下单逻辑...
return true
}
}
// Mock 实现:供 Preview 和测试使用
struct MockOrderService: OrderService {
var mockOrders: [Order]
func fetchOrders() -> [Order] { mockOrders }
func placeOrder(_ order: Order) -> Bool { true }
}
// 通过构造函数注入依赖的 View
struct OrderListView: View {
let orderService: OrderService
@State private var orders: [Order] = []
var body: some View {
List(orders) { order in
OrderRow(order: order)
}
.onAppear {
orders = orderService.fetchOrders()
}
}
}
// Preview 中使用 Mock 数据,秒级渲染
struct OrderListView_Previews: PreviewProvider {
static var previews: some View {
Group {
OrderListView(orderService: MockOrderService(
mockOrders: [
Order(id: 1, title: "拿铁", amount: 28),
Order(id: 2, title: "美式", amount: 18),
]
))
.previewDisplayName("有订单")
OrderListView(orderService: MockOrderService(mockOrders: []))
.previewDisplayName("空列表")
}
}
}
// 通过 Environment 注入全局配置
struct AppConfig {
var isDarkModeEnabled: Bool
var preferredLanguage: String
}
struct ContentView: View {
@Environment(\.appConfig) var config
var body: some View {
Text("当前语言: \(config.preferredLanguage)")
.foregroundColor(config.isDarkModeEnabled ? .white : .black)
}
}
// 自定义 Environment Key
extension EnvironmentKey {
static let appConfig = AppConfig(
isDarkModeEnabled: false,
preferredLanguage: "zh-Hans"
)
}
// Preview 中覆盖 Environment 值
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environment(\.appConfig, AppConfig(
isDarkModeEnabled: true,
preferredLanguage: "en"
))
}
}
最佳实践
- 把每个 View 的依赖控制在 2-3 个以内,如果构造函数参数超过 5 个,说明这个 View 承担了太多职责,应该拆分
- 为所有数据服务协议同时提供真实实现和 Mock 实现,Mock 实现放在独立的测试 target 中
- Preview 数据要覆盖正常、空数据、错误三种状态,不要只测”开心路径”
- 避免在 View 的
body中做异步加载,使用onAppear或taskmodifier 触发数据加载 - 如果 Preview 编译慢,检查是否有不必要的重型 framework 被引入到 Preview target
还有什么值得关注
- Xcode 12 支持在 Preview 中直接调试 SwiftUI 的
App入口,不用再跑完整的 Simulator - Session 提到了一个实用的技巧:用
#if DEBUG包裹 Mock 数据,确保不会打入生产包 - 多 Preview 并排显示可以同时看到 Light Mode 和 Dark Mode 的效果,再也不用来回切换了