Xcode 15.0 新 #Preview 预览让 SwiftUI 界面调试更加悠然自得

概览

从 Xcode 15 开始,苹果推出了新的 #Preview 宏预览机制,它无论从语法还是灵活性上都远远超过之前的预览方式。#Preview 不但可以实时预览 SwiftUI 视图,而且对 UIKit 的界面预览也是信手拈来。

想学习新 #Preview 预览的一些超实用调试小妙招吗?那就"如意如意"随小伙伴们的心意吧!

在本篇博文中,您将学到如下内容:

  1. Xcode 15.0 新预览机制简介
  2. #Preview 让状态初始化如此轻松!
  3. 为什么 #Preview 中不能直接嵌入可变状态? 4 #Preview + @Observable 宏构造可变 @Binding 实参

相信学完本课后,大家对于 Xcode 15+ 预览的使用以及 SwiftUI 界面调试会更加的轻车熟路!

那还等什么呢?让我们马上开始吧!Let's preview!!!;)


1. Xcode 15.0 新预览机制简介

从 Xcode 15 开始,苹果借助于 Swift 5.9 宏(Macro)的"东风",也为我们带来了全新的 #Preview 预览机制。你猜的没错,它其实就是一个宏:

如上所示:我们将 #Preview 宏定义展开为了其原始代码的实现,大家可以清楚的看到 #Preview 宏背地里到底做了些神马。

在 Xcode 15 之前,小伙伴们需要使用遵循 PreviewProvider 协议的 Previews 结构来帮助我们预览指定的 SwiftUI 视图:

swift 复制代码
struct LaunchView_Previews: PreviewProvider {
    static var previews: some View {
        LaunchView()
            .environmentObject(Model())
    }
}

而现在,只需一个 #Preview 即可搞定所有,岂不呜呼快哉:

swift 复制代码
#Preview {
    LaunchView()
        .environmentObject(Model())
}

为了方便起见,我们还可以非常 nice 的将多个定制的 #Preview 预览内容混合在一起显示:

如上所示,为了便于观察我们在 #Preview 中指定了不同预览名称以及预览设备的方向和明暗主题等特性,简直小菜一碟。

2. #Preview 让状态初始化如此轻松!

"理想很骨感,现实却很残酷"。

在实际开发中,不可能所有视图都如此简单。在现实的 App 中视图多半都会与模型(数据)相绑定,这意味着我们在预览它们之前需要创建对应的数据,否则预览就不会达到预期效果,显示将是一片"空空如也"。

比如在 SwiftUI 里我们有一个分类选择视图(V2_ChallengeClassSelectView),所有内置(Built in)的分类都是从数据库中读取的,但前提是我们在数据库中已经初始化了这些分类,这是通过调用如下方法来完成的:

swift 复制代码
V2_ChallengeClassification.initializeData()

所以,我们可能会写出下面的代码以期待 #Preview 预览可以正常工作:

swift 复制代码
@available(iOS 17.0, *)
#Preview {
    V2_ChallengeClassSelectView(selecting: .constant(nil))
        .onAppear {
            try? V2_ChallengeClassification.initializeData()
        }
}

不过可惜的是,以上实现无法得偿所愿。原因是我们 V2_ChallengeClassSelectView 视图中的分类数据必须在其 body 显示之前就准备就绪:

swift 复制代码
@available(iOS 17.0, *)
struct V2_ChallengeClassSelectView: View {
    
    @Binding var selecting: V2_ChallengeClassification?
    
    let builtInClasses = try? V2_ChallengeClassification.allBuiltInClassifications()
}

对于这种情况,#Preview 宏有一个非常简单的解决方案:我们只需在预览内容之前直接调用初始化代码即可:

swift 复制代码
@available(iOS 17.0, *)
#Preview {
    try? V2_ChallengeClassification.initializeData()
    
    return V2_ChallengeClassSelectView(selecting: .constant(nil))
}

运行可以看到,我们已经能够在预览中正确显示初始化之后的所有内置分类了:

3. 为什么 #Preview 中不能直接嵌入可变状态?

大家可能已经发现了,上面示例中的 V2_ChallengeClassSelectView 视图包含一个 selecting 绑定状态:

swift 复制代码
struct V2_ChallengeClassSelectView: View {   
    @Binding var selecting: V2_ChallengeClassification?
}

但在我们的预览中,为了"偷懒"实际向其传入的是一个绑定常量:

swift 复制代码
V2_ChallengeClassSelectView(selecting: .constant(nil))

这样做的后果是:我们无法在预览中改变 selecting 属性的值,也就无法观察到视图中选择所产生的变化了。

小伙伴们可能会觉得,下面的实现可以帮我们摆脱这一问题:

swift 复制代码
@available(iOS 17.0, *)
#Preview {
    
    @State var selecting: V2_ChallengeClassification?
    
    try? V2_ChallengeClassification.initializeData()
    
    return V2_ChallengeClassSelectView(selecting: $selecting)
       
}

遗憾的是这样做"然并卵",毫无用处:

其原因是:与 Xcode 15 之前的旧预览机制类似,嵌入在预览结构中的简单状态实际上是无法被改变的,即使它被 @State 等(可变)限定符所修饰时也是如此。

那么我们如何解决呢?

答案很简单:将可变状态放到 #Preview 外面去!

4 #Preview + @Observable 宏构造可变 @Binding 实参

从 Xcode 15 (Swift 5.9)开始,苹果推出了新的 @Observable 宏帮我们便捷的创建可观察对象。


更多关于 @Observable 宏以及 Observation 框架的详细介绍,小伙伴们可以移步到下面的博文中进一步观赏:


简单来说,我们可以在 #Preview 之外利用 @Observable 宏包裹我们的可变状态,从而可以将其通过绑定传入到对应的视图中去:

swift 复制代码
@available(iOS 17.0, *)
@Observable
class PreviewModel {
    var selecting: V2_ChallengeClassification?
}

@available(iOS 17.0, *)
#Preview {
    @State var model = PreviewModel()
    
    try? V2_ChallengeClassification.initializeData()
    
    return V2_ChallengeClassSelectView(selecting: $model.selecting)
}

注意,在上面的代码示例中我们实际向 V2_ChallengeClassSelectView 视图传递的绑定是 @Observable 可观察对象 model 中的属性。虽然 model 作为 @State 放在了预览内部,不过由于它是一个可观察对象,所以它仍然可以变化自如。

编译运行修改后的代码,我们现在可以在 #Preview 预览界面中顺畅自如的测试 selecting 分类属性改变时的显示逻辑了:

至此,我们通过上面几个小"栗子"对 Xcode 15 中新的 #Preview 预览机制又有了更深刻的领悟,小伙伴们还不赶快给自己点一个大大的赞吧!👍🏻

总结

在本篇博文中,我们介绍了 Xcode 15+ 中新的 #Preview 预览机制,并讨论了如何利用 #Preview + @Observable 宏让 SwiftUI 界面调试更加"如虎添翼"。

感谢观赏,再会啦!8-)

相关推荐
大熊猫侯佩9 小时前
10 个 Xcode 神技:哥谭开发者必学的 IDE 对抗术
xcode·swift·apple
HarderCoder10 小时前
Swift Package Command Plugin 实战:一键生成 Package 元数据
swift
低调小一15 小时前
Swift 语法学习指南 - 与 Kotlin 对比
微信·kotlin·swift
HarderCoder15 小时前
Swift Package Plugin 深度实战:从原理到落地,自动生成字体枚举
swift
iOS阿玮21 小时前
期待iOS开发者加入,共同抵制“苹果税”反垄断招募令!
uni-app·app·apple
东坡肘子1 天前
从开放平台到受控生态:谷歌宣布 Android 开发者验证政策 | 肘子的 Swift 周报 #0101
android·swiftui·swift
HarderCoder1 天前
用 `defer` 管理异步清理:Swift 中的“保险丝”模式
swift
大熊猫侯佩2 天前
冰火岛 Tech 传:Apple Foundation Models 心法解密(上集)
llm·ai编程·swift
HarderCoder2 天前
深入理解 SwiftUI 的 Structural Identity:为什么“换个条件分支”就会丢状态?
swiftui·swift
HarderCoder2 天前
Swift Continuations 完全指南:一口气弄懂 4 种“桥梁”
swift