在某些条件下,有时我们需要创建一个SwiftUI视图。例如,在上面的代码中,我们定义一个HomeView,其可以包含一个ProfileView,如果LogInManager具有与loggedInUser。我们正在尝试使用标准的if语句实现此目标:struct HomeView: View {
@ObservedObject var loginManager: LoginManager
var body: some View {
VStack {
if let user = loginManager.loggedInUser {
ProfileView(user: user)
}
...
}
}
}
不幸的是,此代码在编译时将引发错误:包含控制流语句的闭包不能与函数构建器ViewBuilder一起使用。
由于此处不使用函数关闭器,而是函数构建器,因此我们不能在其中放入任意代码以形成HStack或VStack。那么我们如何摆脱这种情况呢?一种方法是将此类可选内容的处理直接传递给我们正在创建的视图。例如,我们可以传入ProfileView而不是特定的User值,但可以将其设为可选:struct ProfileView: View {
var user: User?
var body: some View {
guard let user = user else {
return AnyView(EmptyView())
}
return AnyView(VStack {
Text(user.name)
...
})
}
}
该代码有效,但不是特别漂亮。为nil用户创建ProfileView没有任何意义。让我们采用另一种方法:使用map到我们可选的User,将其转换为ProfileView:struct HomeView: View {
@ObservedObject var loginManager: LoginManager
var body: some View {
VStack {
loginManager.loggedInUser.map { user in
ProfileView(user: user)
}
...
}
}
}
这已经很漂亮了:当User没有价值时,我们不需要手动提供EmptyView。我们可以再次将特定的值传递给ProfileView,而不是可选的。有可能做得更好吗?关于@ViewBuilder的好消息是,这不是SwiftUI中的某种私有实现,而是一个可访问的属性,我们可以使用它来注释自己的函数和闭包。使用此属性,我们可以编译一个视图Unwrap,该视图将一个可选值作为参数,并使用一个@ViewBuilder标记的闭包将非nil值转换为View:struct Unwrap<Value, Content: View>: View {
private let value: Value?
private let contentProvider: (Value) -> Content
init(_ value: Value?,
@ViewBuilder content: @escaping (Value) -> Content) {
self.value = value
self.contentProvider = content
}
var body: some View {
value.map(contentProvider)
}
}
使用此设计,我们现在可以完全重新设计HomeView的整个结构:struct HomeView: View {
@ObservedObject var loginManager: LoginManager
var body: some View {
VStack {
Unwrap(loginManager.loggedInUser) { user in
HStack {
Text("Logged in as:")
ProfileView(user: user)
}
}
...
}
}
}