SwiftUI中的@ViewBuilder理解与使用

@ViewBuilder是一个属性包装器,也是一个自定义的函数包装器,用于构建一个或多个视图。在 SwiftUI 中,很多地方都使用了 @ViewBuilder,例如 VStackHStackZStackGroup 等。它可以接受多个视图并返回一个单一的组合视图。

比如我们最常用的View协议中的body,就是用了@ViewBuilder,下面是body的定义:

public protocol View {
    associatedtype Body : View
    @ViewBuilder @MainActor var body: Self.Body { get }
}

body属性前已经用@ViewBuilder进行了修饰,这样,我们在body闭包里就可以添加一个或者多个视图,也不需要些return这样的关键字,SwiftUI将接受多个视图并返回一个单一的组合视图。

如果仿写一个类似View的协议,去掉body前修饰的@ViewBuilder

protocol NonViewBuilderView {
  associatedtype Body : View
  @MainActor var body: Self.Body { get }
}


struct ViewBuilderDemo: NonViewBuilderView {
  var body: some View {
    Text("Hi")
  }
}

我们创建界面ViewBuilderDemo,遵守并实现NonViewBuilderView协议,在body中只放入了一个Text文本,运行起来是没问题的。

但是如果在里面放入了不同类型的视图,就会报错了: Branches have mismatching types 'Text' and 'Button<Text>'

在这里插入图片描述
解决这个问题也比较简单,给body加上@ViewBuilder即可:

struct ViewBuilderDemo: NonViewBuilderView {
  @State private var isText: Bool = false

  @ViewBuilder
  var body: some View {
    if isText {
      Text("Hi")
    } else {
      Button("Button") { }
    }
  }
}

@ViewBuilder修饰属性

在开发过程中,有时候为了让body瘦身,会把里面很多的UI逻辑部分抽出来到一个属性当中去,比如下面的代码:

enum ViewType {
  case one
  case two
  case three
}

struct ViewBuilderDemo: View {
  var viewType: ViewType = .one

  var body: some View {
    contentView
  }

  @ViewBuilder
  private var contentView: some View {
    switch viewType {
      case .one:
        Text("One")
      case .two:
        HStack {
          Text("Two")
          Image(systemName: "heart.fill")
        }
      case .three:
        Image(systemName: "heart.fill")
    }
  }
}

ViewBuilderDemo结构体中,我们定义了一个私有变量contentView,这个变量返回了一些根据逻辑要显示的不同的视图。
如果这个contentView属性不加@ViewBuilder修饰,那么必然会报错了。

在这里插入图片描述

@ViewBuilder修饰方法

有些时候我们更喜欢用方法去封装一些UI,比如上面同样的逻辑,改成使用方法。

@ViewBuilder
private func contentViewBaseOnType() -> some View {
  switch viewType {
    case .one:
      Text("One")
    case .two:
      HStack {
        Text("Two")
        Image(systemName: "heart.fill")
      }
    case .three:
      Image(systemName: "heart.fill")
  }
}

原理和修饰属性一样,方法里面也返回了很多类型的组件,一个或者多个,如果不用@ViewBuilder修饰就会报错的。
在这里插入图片描述

@ViewBuilder修饰参数

有时候自定义组件,需要从外部传入具体的显示内容,而组件只是负责一些布局类的逻辑,比如下面这段代码:

struct HeaderView<Content: View>: View {
  let isHorizontal: Bool
  let content: Content

  init(isHorizontal: Bool, content: () -> Content) {
    self.isHorizontal = isHorizontal
    self.content = content()
  }

  var body: some View {
    if isHorizontal {
      HStack {
        content
      }
    } else {
      VStack {
        content
      }
    }
  }
}

HeaderView组件有两个属性,isHorizontalcontent,根据isHorizontal的不同,进而选择不同的排列方式将content显示。这里面的Content采用了泛型,继承了View,如果直接定义一个View类型的content会报错的。

init方法中,传入isHorizontal和构建content的闭包,就目前的定义看似没有问题,但是在调用的时候报错了。
在这里插入图片描述
这里的错误主要是我们初始化方法里面构建content的闭包有问题,这个闭包里面可以放置很多类型的组件,SwiftUI认为这些组件组成的一个大组件不符合View协议。这就和我们上面说的很类似,我们需要告知SwiftUI,构建一个组合视图,修改一下init方法即可,在content参数前加上@ViewBuilder

 init(isHorizontal: Bool, @ViewBuilder content: () -> Content) {
  self.isHorizontal = isHorizontal
  self.content = content()
}

除了这种方法,我们也可以省略init方法,在定义content属性的地方添加@ViewBuilder。重构一下HeaderView组件:

struct HeaderView<Content: View>: View {
  let isHorizontal: Bool
  @ViewBuilder let content: () -> Content

  var body: some View {
    if isHorizontal {
      HStack {
        content()
      }
    } else {
      VStack {
        content()
      }
    }
  }
}

这里将content直接定义为闭包类型,并且前面加上了@ViewBuilder,这样在初始化的时候同样不会报错。

写在最后

@ViewBuilderSwiftUI 中一个非常有用的工具,它极大地简化了视图的组合和复用。通过在属性,方法以及初始化方法上合理使用 @ViewBuilder,可以更有效地构建复杂且高效的用户界面。

最后,希望能够帮助到有需要的朋友,如果觉得有帮助,还望点个赞,添加个关注,笔者也会不断地努力,写出更多更好用的文章。

  • 45
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值