Что такое View в SwiftUI

от автора

Привет, Хабр!

Недавно у меня было собеседование, на котором мы довольно много времени обсуждали одну из фундаментальных концепций 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/