Иногда нам нужно сформировать SwiftUI View, учитывая некоторые условия. Например, в приведенном коде мы определяем HomeView, который может содержать ProfileView, в случае если в LogInManager есть loggedInUser. Мы пытаемся это реализовать, используя стандартный оператор if:
К сожалению, этот код выдаст при компиляции ошибку:
Так как здесь используются не обычные замыкания, а function builders, мы не можем поместить в них произвольный код для формирования HStack или VStack. Так как же нам выйти из положения?
Один из способов — передать обработку таких optionals непосредственно в те view, которые мы формируем. Например, мы можем передавать в наш ProfileView не конкретное значение User, а сделать его optional:
Этот код работает, но не особо красив. Нет никакого смысла создавать ProfileView для пользователя nil. Применим другой подход: используем map к нашему optional User, чтобы преобразовать его в ProfileView:
Так уже гораздо симпатичнее: нам не нужно вручную отдавать EmptyView, когда у User отсутствует значение. Также мы опять можем передавать в ProfileView конкретное значение, а не optional. А можно ли сделать ещё лучше?
Хорошая новость о @ViewBuilder состоит в том, что это не какая-то закрытая реализация в SwiftUI, а доступный атрибут, которым мы можем аннотировать свои собственные функции и замыкания.
Используя этот атрибут мы можем собрать view Unwrap, который принимает в качестве параметров optional значение и помеченную @ViewBuilder замыкание для преобразования не-nil значения во View:
Используя эту конструкцию, мы теперь можем полностью переработать всю структуру HomeView:
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)
}
}
...
}
}
}