POV: на собесе прилетел вопрос - "Зачем мы в коде явно указываем @ViewBuilder?"
POV: на собесе прилетел вопрос - "Зачем мы в коде явно указываем @ViewBuilder?"

Приветствуем вас, уважаемые знатоки! С вами как всегда, уже неизменно, играют наши уважаемые телезрители. И так, сегодня сессия: 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 указанный явно, так скажем, имеет место быть:

Работа с ориентацией экрана

  1. Создадим кастомный стек состоящий из VStack и HStack;

  2. Добавим @Enviroment отслеживающий sizeClass;

  3. В случае поворота экрана, тип возвращаемой ориентации стека должен изменяться.

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 и т.д.

Обход? Конечно есть:

  1. Создаем свой @resultBuilder, со своей реализацией метода .buildBlock();

  2. Расширить @ViewBuilder дополнительными методами .buildBlock();

  3. Использовать вложенность, контейнер в контейнер.

Ну что, финал! Что же мы получим?

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) внутри представления могут влиять как на производительность вашего приложения, так и на анимацию представлений, поэтому их использования внутри лучше избегать.

Список литературы

Комментарии (0)