SwiftUI 6.0(Xcode 16)新 PreviewModifier 协议让预览调试如虎添翼

概览

用 SwiftUI 框架开发过应用的小伙伴们都知道,SwiftUI 中的视图由各种属性和绑定"扑朔迷离"的缠绕在一起,自成体系。

想要在 Xcode 预览中泰然处之的调试 SwiftUI 视图有时并不是件容易的事。其中,最让人秃头码农们头疼的恐怕就要数如何正确的向预览传入视图内部的状态了。

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

  • 概览
  • [1. PreviewModifier 到底有啥用?](#1. PreviewModifier 到底有啥用?)
  • [2. 用 PreviewModifier "点缀"预览视图外观](#2. 用 PreviewModifier “点缀”预览视图外观)
  • [3. PreviewModifier 诞生之前我们如何向预览传送数据?](#3. PreviewModifier 诞生之前我们如何向预览传送数据?)
  • [4. 用 PreviewModifier 注入(inject)预览模拟数据](#4. 用 PreviewModifier 注入(inject)预览模拟数据)
  • 总结

遵循 SwiftUI 6.0(Xcode 16)新推出的 PreviewModifier 协议,正可谓是:"你好,我好,大家都好"。

不信?且看分晓!Let's go!!!😉


1. PreviewModifier 到底有啥用?

从 SwiftUI 6.0 开始,顺便借助 Xcode 16 的东风,苹果推出了全新的 PreviewModifier 协议:

正如该协议"自夸"的那样:PreviewModifier 可以让 Xcode 预览(Preview)界面和调试数据"浑然天成,融洽无间"。

有了 PreviewModifier,我们即可从两个方面来为预览调试"雪中送炭":

  • 统一改变预览中视图的外观;
  • 为预览视图传入模拟测试数据;

下面,就让我们依次来看看它们究竟是如何"大施拳脚"的吧。

2. 用 PreviewModifier "点缀"预览视图外观

假若我们希望 Xcode 预览中某些被调试的视图都放在导航容器中,并且根据实际情况增加导航标题和导航栏 Logo。

注意,这些视图的 body 代码自身并没有嵌入到导航视图内,因为这是使用它们的父视图份内的事儿。这意味着,我们必须繁文缛节的在所有预览中将这些视图嵌入到导航视图中去:

swift 复制代码
#Preview {
    NavigationStack {
        ContentView()
            .navigationTitle("SwiftUI 滚动行为演示")
            .toolbar {
                Text("大熊猫侯佩 @ \(Text("CSDN").foregroundStyle(.red))")
                    .foregroundStyle(.orange)
                    .font(.headline.weight(.heavy))
            }
    }
}

如上代码所示:我们不但要在预览中为每个"潜在的"导航子视图添加导航容器(NavigationStack),还要不厌其烦的为它们设置相应的导航标题和 Logo 视图。

现在,我们看看 PreviewModifier 协议能为我们做些什么吧:

swift 复制代码
struct NavPreviewHelper: PreviewModifier {
    
    var title: String
    
    func body(content: Content, context: Void) -> some View {
        NavigationStack {
            content
                .navigationTitle(title)
                .toolbar {
                    Text("大熊猫侯佩 @ \(Text("CSDN").foregroundStyle(.red))")
                        .foregroundStyle(.orange)
                        .font(.headline.weight(.heavy))
                }
        }
    }
}

如大家所见,我们创建了一个 NavPreviewHelper 结构,并让其遵循 PreviewModifier 协议。我们只需实现其中的 body 方法,然后将预览测试的视图包裹在导航容器(NavigationStack)内部,并妥善为其设置了标题和预览 Logo。

有了 NavPreviewHelper 这位"贤内助",我们可以易如反掌的将任何需要"如此炮制"的预览测试视图嵌入到 NavigationStack 中并妥善"装扮"了:

swift 复制代码
#Preview(traits: .modifier(NavPreviewHelper(title: "SwiftUI 滚动行为演示"))) {
    ContentView()
}

#Preview("另一个视图", traits: .modifier(NavPreviewHelper(title: "另一个视图演示"))) {
    Text("另一个测试视图!")
}

从下面的演示中大家可以看到,我们的 NavPreviewHelper 就像"预览模版"一样,可供任何有此需求的预览视图使用了:

我们甚至还能通过 PreviewTrait 扩展,进一步简化 #Preview 宏中 NavPreviewHelper 的调用:

swift 复制代码
extension PreviewTrait where T == Preview.ViewTraits {
    @MainActor static func navHelper(_ title: String) -> PreviewTrait<T> {
        .modifier(NavPreviewHelper(title: title))
    }
}

#Preview(traits: .navHelper("SwiftUI 滚动行为演示")) {
    ContentView()
}

#Preview("另一个视图", traits: .navHelper("另一个视图演示")) {
    Text("另一个测试视图!")
}

3. PreviewModifier 诞生之前我们如何向预览传送数据?

在 PreviewModifier 降临之前,倘若我们想要为视图传入预览测试数据,在某些场景下非得大费周章一番不可:

swift 复制代码
#Preview {
    @Previewable var clock = Clock.newCountClock(name: "大熊猫侯佩的计时器")
    
    let container = ModelContainer.preview
    try! container.mainContext.save(clock)
    
    return CountClockCell(clockID: clock.id).modelContainer(container)
}

如上代码所示:我们需要为所有使用 Clock 托管实例的预览视图"喋喋不休"的初始化测试数据。虽然使用 SwiftUI 6.0 新加入的 @Previewable 宏能够让实现简洁不少,但在每个预览视图之前都来上这么"一坨"代码,恐怕也绝非长久之计。毕竟,它严重违反了 DRYKISS原则。


关于 SwiftUI 6.0(Xcode 16) 中预览新增 @Previewable 宏的使用,请小伙伴们移步如下链接观赏更详细的内容:


4. 用 PreviewModifier 注入(inject)预览模拟数据

现在,我们回到拥有 PreviewModifier 色彩斑斓的"新世界"中吧。

回忆一下之前 NavPreviewHelper 类型的实现:我们实际上完全忽略了 body 方法中的 context 参数(所以将其类型设置为 Void)。其实,这个 context 可以大有作为。

为了让 context 物尽其用,我们需要另外实现一个 makeSharedContext 方法,用它来注入测试模型数据。

现在,我们尝试将前一个需要 Clock 模型数据的预览改写为 PreviewModifier "助人为乐"的形式:

swift 复制代码
var clockID: UUID?
private struct MockClockData: PreviewModifier {
    static func makeSharedContext() throws -> ModelContainer {
        let container = ModelContainer.preview
        
        let clock = Clock.newCountClock(name: "大熊猫的计时器")
        clockID = clock.id
        try container.mainContext.save(clock)
        
        return container
    }

    func body(content: Content, context: ModelContainer) -> some View {
        content.modelContainer(context)
    }
}

extension PreviewTrait where T == Preview.ViewTraits {
    @available(watchOS 11.0, *)
    @MainActor static var sampleData: Self = .modifier(MockClockData())
}

@available(watchOS 11.0, *)
#Preview(traits: .mockClockData) {
    CountClockCell(clockID: clockID!)
}

从上面的代码可以看到,我们利用 makeSharedContext 方法在指定的 ModelContainer 中生成并保存了所需的测试数据,接下来在 Xcoce 预览中使用这些数据就是水到渠成的事情啦:

有了这位预览"好帮手"之后,我们现在可以为任何需要 Clock 托管数据的预览视图"套用" MockClockData "测试模版"啦,小伙伴们是不是觉得事倍功半,一步到位了呢?棒棒哒!💯

总结

在本篇博文中,我们介绍了如何使用 SwiftUI 6.0(Xcode 16)中最新的 PreviewModifier 协议让预览调试闲情逸致、如虎添翼。

感谢观赏,再会啦!😎

相关推荐
大熊猫侯佩1 个月前
WWDC24(Xcode 16)中全新的 Swift Testing 使用进阶
单元测试·xctest·xcode 16·wwdc 24·swift testing·初始化和清理·测试顺序
大熊猫侯佩1 个月前
用接地气的例子趣谈 WWDC 24 全新的 Swift Testing 入门(三)
单元测试·xcode 16·wwdc 24·swift 宏·swift testing·#expect·#require
大熊猫侯佩2 个月前
SwiftUI 6.0(iOS 18)新增的网格渐变色 MeshGradient 解惑
动画·颜色·ios 18·swiftui 6.0·渐变色·gradient·网格渐变色
大熊猫侯佩2 个月前
SwiftUI 6.0(iOS 18)自定义容器值(Container Values)让容器布局渐入佳境(上)
foreach·group·layout·ios 18·swiftui 6.0·containervalues·自定义容器
Se7en丶潇洒哥3 个月前
Xcode 16 上传AppStore遇到第三方库 bitcode 的问题
ios·xcode·appstore·xcode 16·bitcode
大熊猫侯佩5 个月前
迂回战术:“另类“全新安装 macOS 15 Sequoia beta2 的极简方法
macos·磁盘空间·sequoia·xcode 16·macos 15·全新安装·beta
大熊猫侯佩5 个月前
SwiftUI 6.0(iOS 18.0)滚动视图新增的滚动阶段(Scroll Phase)监听功能趣谈
scrollview·ios 18·xcode 16·swiftui 6.0·滚动视图·scroll phase·监听滚动状态
大熊猫侯佩6 个月前
SwiftUI 6.0(iOS 18/macOS 15)关于颜色 Color 的新玩法
color·ios 18·xcode 16·swiftui 6.0·macos 15·混合颜色·渐变动画
大熊猫侯佩6 个月前
SwiftUI 6.0(iOS 18)ScrollView 全新的滚动位置(ScrollPosition)揭秘
scrollview·ios 18·swiftui 6.0·ipados 18·滚动视图·wwdc24·scrollposition