Приветствуем вас, уважаемые знатоки! С вами как всегда, уже неизменно, играют наши уважаемые телезрители. И так, сегодня сессия: 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) внутри представления могут влиять как на производительность вашего приложения, так и на анимацию представлений, поэтому их использования внутри лучше избегать.
ссылка на оригинал статьи https://habr.com/ru/articles/761722/
Добавить комментарий