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导致的报错,因而转入研究其他的解决方案。
希望能给更多的人带来帮助。