@ViewBuilder Что? Зачем? Когда?

от автора

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/761722/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *