我们从条件开始在SwiftUI中形成View

在某些条件下,有时我们需要创建一个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 {
            // We have to use 'AnyView' to perform type erasure here,
            // in order to give our 'body' a single return type:
            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)
                }
            }
            ...
        }
    }
} 

All Articles