Советы и трюки при использовании SwiftUI ViewBuilder

Swift 6.1 
Swift 6.1 

Тип ViewBuilder — это ключевая часть архитектуры SwiftUI. Именно благодаря ему мы можем описывать несколько представлений (View) в пределах одной области (например, в body или внутри замыканий HStack, VStack и т.д.) без необходимости вручную объединять или оборачивать их.

VStack { Image(systemName: "star") Text("Hello, world!") }

Интересный факт: ViewBuilder можно долго использовать, даже не осознавая, что он вообще существует. Чаще всего мы не указываем его явно — всё уже аннотировано соответствующим атрибутом @ViewBuilder.

Протокол View, который используется для создания пользовательских представлений, сам по себе уже содержит @ViewBuilder в определении body:

@MainActor @preconcurrency public protocol View { associatedtype Body : View @ViewBuilder @MainActor @preconcurrency var body: Self.Body { get } }

Это позволяет нам использовать управляющие конструкции (if, else, switch) прямо в body, даже если ветви возвращают разные типы:

struct RootView: View { @State private var user: User? var body: some View { if let user { HomeView(user: user) } else { LoginView(user: $user) } } }

Когда использовать ViewBuilder напрямую

Хотя @ViewBuilder применяется автоматически, бывают ситуации, когда его прямое использование даёт большую гибкость. Рассмотрим примеры.

Пользовательские контейнеры

Мы можем аннотировать любое свойство, функцию или замыкание с помощью @ViewBuilder, чтобы добавить “DSL-подобные” возможности, как в стандартных элементах SwiftUI.

Допустим, мы хотим создать собственный Container, который содержит заголовок и контент:

struct Container<Header: View, Content: View>: View { var header: Header var content: Content var body: some View { VStack(spacing: 0) { header .frame(maxWidth: .infinity) .padding() .foregroundStyle(.white) .background(Color.blue) ScrollView { content.padding() } } } }

Такой контейнер можно вызвать так:

Container(header: Text("Welcome"), content: ContentView())

Теперь добавим @ViewBuilder:

@ViewBuilder var header: Header @ViewBuilder var content: Content

С этими аннотациями header и content теперь ожидаются как замыкания. Это позволяет использовать управляющие конструкции:

Container(header: { Text("Welcome") }, content: { if let user { HomeView(user: user) } else { LoginView(user: $user) } })

Делание компонентов необязательными

Если нам нужно сделать заголовок необязательным, мы можем создать расширение с EmptyView:

extension Container where Header == EmptyView { init(@ViewBuilder content: () -> Content) { self.init(header: EmptyView.init, content: content) } }

Или — ещё более удобно — задать EmptyView.init как значение по умолчанию:

init( @ViewBuilder header: () -> Header = EmptyView.init, @ViewBuilder content: () -> Content ) { self.header = header() self.content = content() }

Теперь можно вызывать:

Container { if let user { HomeView(user: user) } else { LoginView(user: $user) } }

Обработка нескольких выражений

Если в header мы добавим несколько элементов, например:

header: { Text("Welcome") NavigationLink("Info") { InfoView() } }

SwiftUI обработает это как Group, то есть они будут независимыми по стилю и размещению. Чтобы этого избежать, можно обернуть содержимое в VStack

VStack { header }

И аналогично для content.

Структурирование больших вью через функции

Можно выносить части UI в отдельные приватные функции с @ViewBuilder:

struct RootView: View { var body: some View { Container(header: header, content: content) } @ViewBuilder func header() -> some View { Text("Welcome") NavigationLink("Info") { InfoView() } } @ViewBuilder func content() -> some View { ... } }

Но тут возникает нюанс: header() на самом деле возвращает несколько View, а не один. Поэтому лучше убрать @ViewBuilder и явно обернуть в VStack:

func header() -> some View { VStack(spacing: 20) { Text("Welcome") NavigationLink("Info") { InfoView() } } }

А для content можно оставить @ViewBuilder, если он содержит if/else.

Заключение

ViewBuilder — мощный инструмент SwiftUI, который позволяет писать гибкие и выразительные пользовательские интерфейсы. Используя его в своих компонентах, мы можем создавать API, которые выглядят и ощущаются как нативные SwiftUI-виды. Это улучшает читаемость и поддержку кода.

Начать дискуссию