Приветствуем вас, уважаемые знатоки! С вами как всегда, уже неизменно, играют наши уважаемые телезрители. И так, сегодня сессия: SwiftUI, и с вами играет: Марина, из славного города Мокроперчатск, со следующим вопросом:
«Недавно я была на одном техническом собеседовании, и в разделе про SwiftUI мне задали вопрос — „Зачем в коде мы явно указываем @ViewBuilder“. К сожалению, я не смогла ответить на этот вопрос, может вы знаете...»
Спасибо Марина, и на этом пожалуй стоит закончить с ролевыми играми и перейти к цели статьи, а она у нас следующая:
Цель: Обосновать применение конструктора @ViewBuilder и перечислить возможные кейсы применения, узнать его ограничения.
И так, если говорить просто, @ViewBuilder - это конструктор результатов используемый в синтаксисе библиотеки SwiftUI. Данный конструктор является частью привычного протокола View:
public protocol View {
associatedtype Body : View
@ViewBuilder var body: Self.Body { get }
}
Но:
Зачем?
Какую магию он в себе несет?
И главное, что без него работать не будет?
Давайте попробуем создать структуру, в которой подменим протокол View, на наш кастомные протокол, не использующий @ViewBuilder:
public protocol ViewWithoutViewBuilder {
associatedtype Body : View
var body: Self.Body { get }
}
В случае если мы попробуем подписать наш, уже родной, ContentView под этот протокол, то получим следующее:
struct ContentView: ViewWithoutViewBuilder {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
.padding()
}
}
Шок, но ошибок никаких нет. Давайте немного усложним наш стартовый ContentView, и добавим ветвление того, что мы хотим показать нашему юзеру:
struct ContentView: ViewWithoutViewBuilder {
@State var isGreeting: Bool = true
var body: some View { // #Error! -> Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type
if isGreeting {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
.padding()
} else {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Goodbye, world!")
}
.padding()
}
}
}
Настал момент, когда появилась ошибка. Что же сказал нам компилятор?
«Функция объявляет непрозрачный тип возвращаемого значения, но не имеет в своем теле операторов возврата, из которых можно было бы вывести базовый тип.»
Это возникло в следствии того, что дочерних представлений (вариантов ответа на возврат "some View") стало больше, чем одно. Попробуем пофиксить и сделаем то, что хочет компилятор -> добавим "return":
struct ContentView: ViewWithoutViewBuilder {
@State var isGreeting: Bool = true
var body: some View {
if isGreeting {
return VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
.padding()
} else {
return VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Goodbye, world!")
}
.padding()
}
}
}
В таком случае, ошибка и в правду уйдет, но не может ведь быть, что мы используем @ViewBuilder только для того, чтобы избавиться от оператора возврата "return"?
Давайте продолжим пробовать различные вариации, и в одной из таковых попробуем поменять один из возвращаемых типов:
struct ContentView: ViewWithoutViewBuilder {
@State var isGreeting: Bool = true
var body: some View { // #Error -> Function declares an opaque return type 'some View', but the return statements in its body do not have matching underlying types
if isGreeting {
return VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
.padding()
} else {
return VStack {
//MARK: - Просто удалим в одном из стеков наш Image
Text("Goodbye, world!")
}
.padding()
}
}
}
Опять ошибка. Что у нас на этот раз говорит компилятор?
«Функция объявляет непрозрачный тип возвращаемого значения „некоторый вид“, но операторы возврата в ее теле не имеют соответствующих базовых типов.»
Другими словами:
"А вот это уже слишком сложно, вернуть надо разное, что мне вернуть-то???
Вот и практически доказанный ответ по области применения @ViewBuilder:
Ответ: «Потребность в использовании @ViewBuilder возникает тогда, когда наше представление (нечто возвращающее some View) имеет внутреннее ветвление, и в кейсах этого ветвления возвращаются разные по структуре представления.
struct ContentView: ViewWithoutViewBuilder {
@State var isGreeting: Bool = true
@ViewBuilder var body: some View {
if isGreeting {
return VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
.padding()
} else {
return VStack {
Text("Goodbye, world!")
}
.padding()
}
}
}
Теперь когда мы ответили на вопросы: "Что?" и "Зачем?", можно перейти к кейсу "Когда" @ViewBuilder указанный явно, так скажем, имеет место быть:
Работа с ориентацией экрана
Создадим кастомный стек состоящий из VStack и HStack;
Добавим @Enviroment отслеживающий sizeClass;
В случае поворота экрана, тип возвращаемой ориентации стека должен изменяться.
struct VorHStack<Content: View>: View {
@Environment(\.horizontalSizeClass) var horizontalSizeClass
let content: Content
init(@ViewBuilder _ content: () -> Content) {
self.content = content()
}
var body: some View {
if horizontalSizeClass == .compact {
VStack { content }
} else {
HStack { content }
}
}
}
Исходя из написанного кода возможно следующее применение:
struct ContentView: View {
var body: some View {
VorHStack {
Text("Hello, World!")
Text("Hello, World2!")
}
}
}
Работать это будет отлично, но мы можем это сделать более компактным. Но как?
Для этого давайте провалимся внутрь одного из выше указанных контейнеров:
struct VStack<Content> : View where Content : View {
@inlinable public init(
alignment: HorizontalAlignment = .center,
spacing: CGFloat? = nil,
@ViewBuilder content: () -> Content
)
public typealias Body = Never
}
Мы видим, что "content", требуемый дефолтным инициализатором, принимает в себя "() -> Content", а так же у него тоже явно указан @ViewBuilder. Раз уж мы сегодня проваливались почти везде, то почему до сих пор не провалились в сам @ViewBuilder? Погнали!
@resultBuilder public struct ViewBuilder {
public static func buildBlock() -> EmptyView
public static func buildBlock<Content>(_ content: Content) -> Content where Content : View
}
...
extension ViewBuilder {
public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View, C7 : View, C8 : View, C9 : View
}
Здесь новым для нас будет атрибут @resultBuilder. Им помечают все создаваемые конструкторы. Пролистав все экстеншены мы все таки добрались до финальной вариации функции buildBlock() и обнаружили, что максимальное количество элементов, которых она в себя может принять - 10!
Значит тут мы уже наткнулись на ограничения по применению @ViewBuilder - внутри не может лежать более 10 аргументов. Таким образом, стоит запомнить, что это применимо для всех типов использующих @ViewBuilder - VStack, HStack, ZStack, List, Group и т.д.
Обход? Конечно есть:
Создаем свой @resultBuilder, со своей реализацией метода .buildBlock();
Расширить @ViewBuilder дополнительными методами .buildBlock();
Использовать вложенность, контейнер в контейнер.
Ну что, финал! Что же мы получим?
struct VorHStack<Content: View>: View {
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@ViewBuilder let content: () -> Content
var body: some View {
if horizontalSizeClass == .compact {
VStack(content: content)
} else {
HStack(content: content)
}
}
}
По-моему получилось просто и лаконично, перейдем к итогам.
Заключение
И так, мы вывели определение для @ViewBuilder, а так же рассмотрели кейс, когда это применение оправдано, а когда ограничено. Так же стоит обратить внимание, что используемые операторы ветвлений (if) внутри представления могут влиять как на производительность вашего приложения, так и на анимацию представлений, поэтому их использования внутри лучше избегать.