Swift UI #Preview预览传参报错
Swift UI #Preview预览传参报错

Swift UI #Preview预览传参报错

Swift当前视图需要一个@Binding变量关联从外部视图传进来的参数。但是Swift UI默认生成的#Preview预览环境会因为缺少@Binding变量而报错。

报错代码:

struct privacyPolicyPage: View {
    @Binding var privacyPolicy: Bool
    var body: some View {
    }
}

#Preview {
    privacyPolicyPage(privacyPolicy: true)
}

相关报错如下:

1、Cannot convert value of type 'Bool' to expected argument type 'Binding<Bool>'
2、Result of 'privacyPolicyPage' initializer is unused
3、/var/folders/1y/t7s8thm536j0hgvd3fhdp7pr0000gn/T/swift-generated-sources/@__swiftmacro_7ERdepot33_CF7EAC2C05B5DB522500A8179B7A0D4DLl7PreviewfMf_.swift:14:9 Ambiguous use of 'init(_:traits:body:)'

以上三种报错都是在#Preview代码中显示的。

具体的原因就是我们的视图和预览视图是分开的,预览视图#Preview用于在Xcode中快速测试和查看视图,所以可能会存在一些兼容问题。我们从外部视图传到视图当中,预览视图会因为缺少传参而报错。

解决方案1

在#Previw中声明一个@Binding变量并绑定到预览视图当中。

#Preview {
    privacyPolicyPage(privacyPolicy: .constant(true))
}

注:.constant可以生成一个绑定值,简化开发。

解决方案2

使用PreviewProvider协议的结构解决该问题。

struct privacyPolicyPage_Previews: PreviewProvider {
    static var previews: some View {
        privacyPolicyPage(privacyPolicy: .constant(privacyPolicy))
    }
}

在这个代码中,我们定义了一个PreviewProvider协议的结构,确保预览代码更清晰和稳定。通过.constant()的形式将绑定值传入到我们的视图当中。

类似的预览报错问题也可参考上述代码,previews视图中使用.constant生成绑定值,将示例中的视图替换成自己的视图。

解决方案3:自定义结构

当你的参数不是简单的Bool、String类型,而是自定义的结构/类时,也可以通过#Preview来实现:

#Preview {
    HomeView(updateER: .constant(UpdateER()))
}

或使用遵守PreviewProvider协议的结构,来处理这一问题。

struct HomeView: View {
    @Binding var updateER: UpdateER
    var body: some View {
    }
}

在上面代码示例中, HomeView视图中的UpdateER类型为,自定义的同步汇率结构,因此在预览的previews视图中,我们也可以通过constant绑定一个实例:

struct privacyPolicyPage_Previews: PreviewProvider {
    static var previews: some View {
        // 在 preview 内部初始化
        HomeView(updateER: .constant(UpdateER()))
    }
}

问题到这里就得到了解决。

=========分割线==========

下面的内容是关于PreviewProvider的问题,我们的#Preview预览报错的问题已经可以通过constant来解决。

进一步延伸的原因是,自己在排查和解决这一报错问题时,走了一些弯路,但也学习的很多知识,因此进一步将这些问题分享出来,让更多人了解其他的知识。

问题延伸

延伸1:@State的使用

当我们尝试下面的代码预览时,Xcode不会报错:

struct privacyPolicyPage_Previews: PreviewProvider {
    @State static var updateER = UpdateER()
    static var previews: some View {
        HomeView(updateER: $updateER)
    }
}

但这里的实际问题在于这个写法具有潜在的问题,SwiftUI的@State属性包装器只应该用于实例属性,而不是static静态属性。

在static静态属性中声明@State,会导致SwiftUI无法管理状态的变更,从而无法正确更新UI。

struct privacyPolicyPage_Previews: PreviewProvider {
    static var updateER = UpdateER()
    static var previews: some View {
        HomeView(updateER: $updateER)
    }
}

当我们尝试去除@State后,updateER就不再是一个状态变量,而是一个普通的变量,在SwiftUI中,$updateER需要对@State或其他的类型包装器(如@Binding)的引用,所以如果updateER不用@State包装,就无法通过$updateER获取绑定。

Cannot find '$updateER' in scope

在这种情况下,SwiftUI就会找不到updateER绑定。有人可能会疑问可不可以去除$updateER前面的绑定值:

HomeView(updateER: updateER)

这种方式是不行的,因为前面提到了HomeView()当中的updateER是一个@Binding类型,需要从外面传进来一个绑定值才可以使用。

所以,我们需要知道的是@State属性包装器不应该使用到static静态属性上。此外,还需要注意的一点是,@State属性只能在视图的主体(结构体顶层)中声明,而不能在局部作用域(如函数题内)中。

这个问题在于@State属性必须是视图结构的一部分,SwiftUI才能正确的检测和相应状态变化。@State如果放在局部作用域中(如函数内部),每次调用该函数都会重新创建@State,导致SwiftUI无法在不同的函数调用之间正确持久化和管理这些状态,导致状态丢失和不可预知的行为。

延伸2:PreviewProvider不允许private属性

struct privacyPolicyPage_Previews: PreviewProvider {
    static var previews: some View {
        @State var updateER = UpdateER()
        HomeView(updateER: $updateER)
    }
}

这段代码是将updateER定义在previews静态变量当中,但也没有报错。

这里的问题首先是前面的违法@State属性的特性,没有定义在视图结构的顶层声明。

同时,当我们尝试使用之前的@State private时,也会报错:

@State private var updateER = UpdateER()

Attribute 'private' can only be used in a non-local scope

这是因为PreviewProvider协议要求previews属性具有相同或更高的访问权限。因为我们使用的是private私有,编译器会认为previews的可访问性不高而报错,因此,我们应该去除掉updateER实例的private(当然,这里也是有问题,解决方案还是使用前面提到的constant)。

延伸3:#Preview和PreviewProvider的区别

PreviewProvider是一个协议,要求实现一个static var previews,并返回一个或多个SwiftUI视图,所以我们看到了遵循PreviewProvider的结构和实现的previews属性并返回一个View,用于预览。

struct privacyPolicyPage_Previews: PreviewProvider {
    static var previews: some View {
    }
}

#Preview是Swift 5.9引入的新特性,相比PreviewProvider,#Preview更简洁,不需要定义协议实现。

因此,下面的#Preview和上面的PreviewProvider功能上几乎是等价的。

#Preview {
}

因此,我们的解决方案也可以使用

#Preview {
    HomeView(updateER: .constant(UpdateER()))
}

总结

在 #Preview 中预览 @Binding 视图时,推荐使用 .constant 创建一个临时绑定,传递给需要 @Binding 的视图。

#Preview {
    HomeView(updateER: .constant(UpdateER()))
}

也可以使用PreviewProvider协议来解决这一问题:

struct privacyPolicyPage_Previews: PreviewProvider {
    static var previews: some View {
        HomeView(updateER: .constant(UpdateER()))
    }
}

在解决这一问题时,实际上绕了一个大圈,刚开始已经#Preview无法解决自定义结构的绑定,因为我尝试时有报错,所以找到了PreviewProvider的结局方案。

从遇到问题、到学习问题发生的原因,都进一步加深了我对于这一知识的理解,最后回过头来发现#Preview还是可以通过constant来进行绑定。可能是最开始时,我的constant输入成contant导致的报错,因而转入研究其他的解决方案。

希望能给更多的人带来帮助。

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开放者。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注