Формируем View в SwiftUI, исходя из условий +3


Иногда нам нужно сформировать SwiftUI View, учитывая некоторые условия. Например, в приведенном коде мы определяем 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)
            }

            ...
        }
    }
}

К сожалению, этот код выдаст при компиляции ошибку:
Closure containing control flow statement cannot be used with function builder ViewBuilder.

Так как здесь используются не обычные замыкания, а function builders, мы не можем поместить в них произвольный код для формирования HStack или VStack. Так как же нам выйти из положения?

Один из способов — передать обработку таких optionals непосредственно в те view, которые мы формируем. Например, мы можем передавать в наш ProfileView не конкретное значение User, а сделать его optional:

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)
            ...
        })
    }
}

Этот код работает, но не особо красив. Нет никакого смысла создавать ProfileView для пользователя nil. Применим другой подход: используем map к нашему optional User, чтобы преобразовать его в ProfileView:

struct HomeView: View {
    @ObservedObject var loginManager: LoginManager

    var body: some View {
        VStack {
            loginManager.loggedInUser.map { user in
                ProfileView(user: user)
            }
            ...
        }
    }
}

Так уже гораздо симпатичнее: нам не нужно вручную отдавать EmptyView, когда у User отсутствует значение. Также мы опять можем передавать в ProfileView конкретное значение, а не optional. А можно ли сделать ещё лучше?

Хорошая новость о @ViewBuilder состоит в том, что это не какая-то закрытая реализация в SwiftUI, а доступный атрибут, которым мы можем аннотировать свои собственные функции и замыкания.

Используя этот атрибут мы можем собрать view Unwrap, который принимает в качестве параметров optional значение и помеченную @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)
                }
            }
            ...
        }
    }
} 




К сожалению, не доступен сервер mySQL