Привет, Хабр!
Недавно у меня было собеседование, на котором мы довольно много времени обсуждали одну из фундаментальных концепций SwiftUI — View.
На первый взгляд вопрос кажется простым: что такое
View?
Но очень быстро этот вопрос приводит к более интересным темам. Почему body может возвращать разные представления в зависимости от условия, хотя в случае с обычным some Protocol мы, как правило, не можем возвращать разные конкретные типы из разных веток?
Спойлер: View — это протокол. Но не совсем обычный.
Мы также обсуждали, что на самом деле происходит в SwiftUI при изменении состояния. Перерисовывает ли SwiftUI весь экран? Создаёт ли заново все элементы интерфейса? Или под капотом происходит что-то более тонкое?
В этой статье я хочу шаг за шагом разобрать эти вопросы и объяснить — или хотя бы попытаться объяснить, — что на самом деле представляет собой SwiftUI.View, как работает some View, почему важен ViewBuilder и каким образом SwiftUI обновляет интерфейс при изменении состояния. Я также добавила небольшую визуальную шпаргалку с ключевыми идеями из статьи, чтобы позже можно было быстро освежить основные моменты.
Давайте разберёмся во всём по порядку.
Что такое View?
В SwiftUI View — это протокол. На первый взгляд всё кажется довольно просто: если тип соответствует протоколу View, значит его можно использовать в качестве представления SwiftUI. Но View — это не совсем обычный протокол. Это протокол с ассоциированным типом (Protocol with Associated Type, или PAT).
Если упростить его объявление, оно будет выглядеть примерно так:
public protocol View { associatedtype Body: View @ViewBuilder var body: Self.Body { get }}
Самое важное здесь — вот эта строка:
associatedtype Body: View
Это означает, что каждый тип, соответствующий протоколу View, должен описывать, из чего состоит его body.
Например:
struct MyView: View { var body: some View { Text("Hello") }}
MyView соответствует протоколу View, но само представление, которое будет отображено на экране, определяется его свойством body.
В данном случае body возвращает Text. Поэтому SwiftUI-представление можно воспринимать как легковесное значение, которое описывает другое представление.
View — это не обязательно реальный UI-элемент, уже находящийся на экране. Скорее, это описание того, как интерфейс должен выглядеть.
В этом и заключается одна из ключевых идей SwiftUI:
View — это описание интерфейса, а не конечный отрисованный UI-объект.
Но подождите… почему тогда в body работает if/else?
Теперь мы знаем, что body возвращает some View.
И мы также знаем, что some View не означает «любое представление». Это означает: один конкретный тип, соответствующий протоколу View, но скрытый от нас.
И здесь может возникнуть закономерный вопрос:
Подождите. Но мы ведь постоянно пишем такой код:
struct MyView: View { let isLoggedIn: Bool var body: some View { if isLoggedIn { Text("Welcome back!") } else { Button("Log in") { print("Login tapped") } } }}
Примечание — тут я не пытаюсь писать идеальный продуктовый код, тут я пишу такой код, который наглядно продемонстрирует суть рассматриваемой проблемы 🙂
Это явно разные типы. Но разве мы только что не сказали, что some View должен представлять один конкретный тип? Чтобы лучше увидеть проблему, попробуем реализовать ту же идею вне SwiftUI:
protocol Animal { func makeSound()}struct Dog: Animal { func makeSound() { print("Woof") }}struct Cat: Animal { func makeSound() { print("Meow") }}func makeAnimal(_ isDog: Bool) -> some Animal { if isDog { Dog() } else { Cat() }}
Этот код не скомпилируется
Причина в том, что some Animal — это непрозрачный возвращаемый тип. Он позволяет скрыть конкретный тип, но во всех ветках этот тип всё равно должен оставаться одним и тем же. Поэтому такой код недопустим:
true branch -> Dogfalse branch -> Cat
Потому что Dog и Cat — это разные конкретные типы. Но тогда почему аналогичный пример в SwiftUI работает?
Ответ — @ViewBuilder
body — это не просто обычное свойство, возвращающее some View.
В SwiftUI body вычисляется в специальном контексте result builder. Если упростить объявление протокола View, оно будет выглядеть примерно так:
public protocol View { associatedtype Body: View @ViewBuilder var body: Self.Body { get }}
В более новых интерфейсах SwiftUI это также можно встретить в виде @ContentBuilder, где ContentBuilder — это typealias для ViewBuilder. Именно этот builder и меняет всё.
ViewBuilder — это result builder. Он преобразует код внутри body в один итоговый тип представления.
Поэтому, когда мы пишем:
var body: some View { if isLoggedIn { Text("Welcome back!") } else { Button("Log in") {} }}
SwiftUI не возвращает напрямую ни Text, ни Button.
Вместо этого ViewBuilder преобразует обе ветки в один конкретный тип «под капотом».
Концептуально это можно представить примерно так:
_ConditionalContent<Text, Button<Text>>
Обычно мы не пишем этот тип вручную и даже не задумываемся о нём. Но здесь важно понять главное:
Text и Button — это разные конкретные типы. Однако ViewBuilder оборачивает их в единый результирующий тип, который тоже соответствует протоколу View.
Поэтому body по-прежнему удовлетворяет требованиям some View: он возвращает один конкретный тип. Просто это уже не Text или Button напрямую, а сгенерированный ViewBuilder тип-обёртка.
Тот же самый принцип работает и тогда, когда мы объявляем сразу несколько дочерних представлений:
var body: some View { Text("Title") Text("Subtitle")}
На первый взгляд кажется, что мы возвращаем сразу два представления.
Но ViewBuilder объединяет их в один результирующий тип представления. Исторически для этого использовались такие типы, как TupleView.
Именно здесь кроется ключевая идея:
Сам по себе
some Viewне позволяет возвращать разные типы. Это делает@ViewBuilder.
Или, если быть точнее, @ViewBuilder преобразует разные ветки условий и несколько дочерних представлений в один конкретный тип View, который затем и возвращается как some View.
Эта статья получилась немного длиннее, чем я планировала, поэтому на этом пока остановимся.
Мы разобрали:
-
что такое
Viewв SwiftUI; -
почему
some Viewне означает «любойView»; -
как
ViewBuilderпозволяет SwiftUI сохранять один конкретный возвращаемый тип, даже если мы используем условные конструкции или несколько дочерних представлений.
Но осталась ещё одна важная тема — дерево представлений (rendering tree) и то, что именно SwiftUI обновляет при изменении состояния. Об этом я подробнее расскажу в следующей статье.
А если вы хотите глубже разобраться в теме уже сейчас, ниже я оставлю ссылки на официальную документацию Apple.
https://developer.apple.com/documentation/swiftui/view?source=——45160a184041—————————————
https://developer.apple.com/documentation/swiftui/view/body-8kl5o?source=——45160a184041—————————————

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