Если есть одна вещь, отличающая SwiftUI от других похожих декларативных фреймворков пользовательского интерфейса, таких как React, то это использование системы статических типов. Тип View
делает сравнение элементов иерархии интерфейса быстрым и однозначным. Но как именно это сравнение - диффинг (анг. diffing) - работает? И как именно работает сравнение экземпляров представлений в SwiftUI?
Недавно я запустил SwiftTUI - клон SwiftUI для текстовых терминальных приложений. Для его реализации мне нужно было точно выяснить, как происходит диффинг в SwiftUI. В этой статье я представляю вам свои выводы.
Декларативные фреймворки пользовательского интерфейса и дифферинг идут рука об руку. При декларативном программировании пользовательского интерфейса вместо непосредственного изменения элементов пользовательского интерфейса, отображаемых на экране, вы изменяете некоторое состояние приложения. На основе нового состояния декларативный фреймворк должен будет вычислить, как это изменение повлияет на видимый пользователю UI.
Фреймоворк не может заменить все старые элементы интерфейса на новые после каждого изменения состояния приложения из-за сложной иерархичной структуры UI: многие элементы имеют свои дочерние элементы, которые также имеют своё состояние и так далее. Фрейморку необходимо знать, был ли удалён или вставлен дочерний элемент, чтобы увидеть в итоге, нужно ли нам оставить текущее состояние или создать новое элемента более высокого уровня.
Фреймворку также необходимо знать, какие именно элементы интерфейса добавляются или удаляются, чтобы обновить (или не обновлять) элемент-контейнер, в котором они находятся. Например, стэки могут менять вёрстку своих дочерних элементы, если их содержимое изменилось, а список будет реагировать на изменения только при вставке или удалении элемента, и если только изменяемые элементы находятся в его видимой области. В дополнение ко всему, если любое изменение иерархии можно вычислить, то его же можно попутно анимировать.
Основная проблема вычислений различий заключается даже не в их производительности, но в том, что сами различия могут быть неоднозначными.
Возьмём для примера список с двумя лейблами:
→ List
→ Label("Один")
→ Label("Два")
Пусть он изменится на следующий список:
→ List
→ Label("Два")
→ Label("Три")
Что здесь изменилось? Возможно, метка с надписью “Один” была удалена из начала списка, а метка с надписью “Три” была добавлена в конце. Или, может быть, текст первого лейбла был изменен с “Один” на “Два”, а второй - с “Два” на “Три”. На самом деле и то, и другое может быть правдой!
Фреймворк может отдавать предпочтение одному типу изменений перед другим. Или он может потребовать, чтобы элементы пользовательского интерфейса имели идентификаторы, чтобы он мог их различить. React оставляет этот выбор за программистом.
Если у вас есть список элементов интерфейса с идентификаторами, вам потребуется алгоритм кратчайшего сценария редактирования (shortest-edit-script), такой как алгоритм Майерса, который может найти наименьший набор вставок и удалений для перехода от одной последовательности к другой.
Для большинства изменений SwiftUI не нужен такой алгоритм. Ему также не нужны идентификаторы для представлений, чтобы исключить двусмысленность. Фреймворк полагается исключительно на статическую типизацию View
.
Официальная документация к View
в SwiftUI гласит:
Это тип, который отвечает за представление элемента интерфейса вашего приложения и предоставляет модификаторы для конфигурации этого элемента.
Это описание слегка вводит в заблуждение, так оно имеет смысл для таких типов View
, как Text
, Color
или, например, фигуры (протокол Shape
). Но другие элементы, например такие как ForEach
, являются композициями из других элементов. Такие элементы по-прежнему являются View
, но они репрезентуют сразу несколько элементов пользовательского интерфейса приложения.
Особенно странным в таком случае является EmptyView
. EmptyView
- это такое представление, которое не представляет никакого элемента пользовательского интерфейса. Если вы поместите в список один лишь EmptyView
, то в списке не будет вообще никаких ячеек.
Другие интересные View
- те, которые создаются с помощью конструкторов представлений ViewBuilder
. Конструкторы представлений могут иметь внутри себя if-statements ветвления, и поэтому элементы, построенные с их помощью, могут быть непостоянными динамическими наборами из других элементов интерфейса, таких как ForEach
.
В коде, View
определяется протоколом, для которого единственное требование — это наличие body, который также является View
.
protocol View {
associatedtype Body: View
var body: Body { get }
}
Это не может продолжаться вечно: должны быть View
, которые не определяются через другие View
. Я назову такие View
Примитивными. Обычные View
, построенные из других View
, назову Составными. Примитивные View
имеют тип body
–Never
и вызов у них body
приведет к сбою. Такие View
построены на логике, скрытой из публичного интерфейса SwiftUI.
Как пользователи фреймворка SwiftUI, мы можем создавать только составные View
. Составные View
могут иметь состояние и могут рассматриваться как функция этого состояния, преобразующая его в другое View
. Составные View
используют примитивные View
в своем body. Примитивные View
являются строительными блоками для любых типов View
.
Процесс сравнения обычно начинается с составного View
, когда его body
нужно переоценить в ответ на изменение состояния. SwiftUI затем имеет старое и новое body
, состоящие из какой-то комбинации примитивных и составных View
. Необходимо провести их сравнение и отреагировать на изменения. Тип старого и нового body
всегда одинаков.
Процесс сравнения также обычно заканчивается составными View
, что является оптимизацией производительности в SwiftUI. Если body
недействительного View
содержит другое составное View
, свойства которого не изменились, то алгоритму не нужно дальше оценивать body
этого View
, и работа завершена.
Таким образом, SwiftUI в основном сравнивает оставшиеся примитивные View
, чтобы выяснить, что изменилось, делая их самыми важными в алгоритме сравнения. Составные View
запускают процесс сравнения и предоставляют новые body
, которые нужно сравнить. Они также могут остановить дальнейшие вычисления. Но примитивные View
всё равно нужно сравнить, чтобы выяснить, какие изменения произошли.
Тот факт, что View
представляет собой ряд элементов пользовательского интерфейса, не обязательно означает, что все эти элементы будут присутствовать на экране, когда отображается этот View
. Ленивые представления (lazy views), такие как List
, подгружают новые элементы пользовательского интерфейса из вложенных View
только в случае, если они находятся или будут находиться в видимой области скролла. Элементы пользовательского интерфейса, которые может сгенерировать View
, мы можем называть displayables
. View
в SwiftUI
может иметь ноль или более displayables
.
Когда речь идет о вычислении разницы между новым и старым body
, мы можем разделить примитивные View
, на четыре базовые категории:
Unary views
— Унарные представления
: View
с одним displayable
, на практике это
shapes
— различные формы и фигурыcolors
— цвета и заливки,controls
— элементы управления: кнопки, переключатели, слайдеры и другиеlabels
— разные виды текстаStructural views
— Структурные представления
: View
, которые принимают ноль или более других View
и создают одно новое View
из комбинации их displayables
, причем необязательно из всех сразу. Примеры: ForEach
, EmptyView
и View
, используемые во ViewBuilder
, таких как TupleView
и _ConditionalView
.Container views
— Представления-контейнеры
: View
, которые принимают displayables
другого View
и управляют ими, решая, должны ли они отображаться и как именно они должны быть расположены. Примеры: HStack
, VStack
, List
, LazyVStack
.Modifiers
— Модификаторы
: View
, которые принимают одно другое View
и изменяют расположение или внешний вид всех его displayables
по отдельности. Примеры: View
, которые генерируют модификаторы, такие как .border
, .padding
, .frame
, являющиеся типом ModifiedContent
.Вы можете найти типы структурных View
, которые создают ViewBuilder
, в документации.
Множественные выражения комбинируются в
TupleView
.if-statements
ветвления безelse
создают опциональныеView
, которые сами по себе являютсяView
.if-statements
ветвления сelse
становятся_ConditionalView
.
Container views (Структурные представления)
берут displayables
обернутого ими View
и выводят их на экран. Я называю графическими элементами — graphic
– такие displayable
элементы, которые могут быть отображены на экране. HStack
и VStack
всегда создают graphic
для всех displayable
, которые располагаются в них. Другие контейнеры, такие как List
, являются ленивыми и не превращают все displayables
в graphic
сразу. Контейнерные View
представляют себя как View
с одним displayable
для родительской View
, находящейся выше в иерархии представлений.
Modifiers (Модификаторы)
, применяемые к View
, создают ModifiedContent
. Модификаторы применяют свой эффект ко всем displayables
другого View
по отдельности. Например, модификатор .border
, примененный к TupleView
, добавит границу ко всем displayables
внутри TupleView
. Это означает, что модификаторы могут иметь displayables
, которые выводятся на экран несколько раз, так что один displayable
превращается в несколько graphic
.
Каждая иерархия View
содержит примитивные View
, и каждый примитивный View
будет сочетанием типов View
из перечисленных выше категорий. Рассмотрим этот View
, body
которого содержит только примитивные View
.
struct MyView: View {
@State var showSecret = false
var body: some View {
VStack {
Group {
Button("Show secret") { showSecret.toggle() }
if showSecret {
Text("Secret")
} else {
Color.red
}
}
.padding()
}
}
}
SwiftUI
будет отслеживать иерархию View
приложения во время выполнения, в структуре, которую я называю графом View
. Граф View
хранит ссылки на View
, из которых он был построен, и может хранить состояние (state
) для этих View
.
Для приложения, использующего MyView
, часть графа View
, связанная с MyView
, будет выглядеть так:
Я раскрасил узлы в этом графе View
в зависимости от категории View
в них.
Графы View
для примитивных View
очень похожи по структуре на инициализированные структуры View
сами по себе, но они не совсем одинаковы. Составные View
имеют дочерний узел графа для их body
. View
в ленивых контейнерах могут быть не полностью вычисленны в определенный момент времени.
Очень интересными в этом смысле являются модификаторы
.overlay
и.background
. В этом случаеView
, используемое в качестве.overlay
или.background
, имеет отдельное состояние для каждогоgraphic
(графического элемента), на который он наложен. Узлы графа таких модификаторов будут иметь столько дополнительных дочерних узлов, сколько у нихgraphic
.
Графы View
не являются статичными, и основная часть процесса сравнения, который выполняет SwiftUI
, заключается в том, чтобы выяснить, как обновить граф View
. В примере с MyView
, если переменная showSecret
изменяется, то Text
под узлом _ConditionalView
в графе View
заменяется узлом Color
.
Структура графов View
может изменяться только ограниченным числом способов:
_ConditionalView
меняется с отображения TrueContent
на FalseContent
или наоборот. Обычно это является результатом изменения булевой переменной в составном View
, что и приводит к тому, что body
у View
использует другую ветку if-statement
выражения.
Optional<View>
меняется с отображения ничего на отображение чего-то или наоборот. Обычно это является результатом изменения булевой переменной в составном View
, что приводит к тому, что body
у View
использует или не использует содержимое внутри if-statement
выражения.ForEach
изменяются так, что необходимо обновить его дочерние элементы. Тип дочерних элементов не может изменяться.AnyView
изменяется. Старое содержимое удаляется с экрана, и добавляется новое содержимое.Во всех четырех случаях целые подграфы View
добавляются или удаляются из-под узлов графа View
. Связи узлов графа View
не могут изменяться никаким другим способом. Вы никогда не cможете добавить или удалить модификатор View
. Также вы никогда не можете переместить View
к другому родительскому элементу в графе.
Не все View
в графе View
играют роль в макете, а те, которые играют, могут выполнять разные роли. Мы можем визуализировать displayables
из MyView
в следующем Layout tree
, в дереве макета:
Поскольку граф View
представляет все View
во время выполнения программы, то SwiftUI
должен быть способен построить дерево макета из имеющегося графа View
. При этом во время сравнения потребуется вычислить, как именно изменяются displayables
в дереве макете.
Обратите внимание на то, что граф View
и дерево макета связаны, но не одинаковы. Структурные View
не имеют собственных displayable
и сами отсутствуют в дереве макета. Модификаторы являются родителями для структурных View
в графе View
, но родителями каждого displayable
в дереве макета являются только unary
View
.
Существует несколько способов, которыми graphic
— графические элементы — могут быть отображены: SwiftUI
может создать экземпляры UIView
или CALayer
для их отображения в иерархии представлений приложения. Но при этом стоит заметить, что некоторые View
, такие как HStack
и VStack
, хотя и участвуют в макете, не получают ни UIView
, ни CALayer
.
SwiftUI
нужно выполнить сравнение элементов, чтобы выяснить, как изменить граф View
и как обновить дерево макета. Примитивные View
играют самую важную роль, и существует лишь небольшое количество структурных View
, в узлах которых может изменяться граф View
. Давайте подробнее рассмотрим, как SwiftUI
может использовать статические типы для этого.
Мы знаем, что View
в SwiftUI
может генерировать список displayables
, поэтому мы можем создать собственный протокол View
, где мы явно это укажем. Для процедуры сравнения мы также добавим функцию update
, чтобы View
мог помочь нам определить, какие изменения в графе View
и дереве макета нужно сделать.
protocol View {
associatedtype Body: View
var body: Body { get }
var displayables: [Displayable] { get }
func update(using view: Self, offset: Int)
}
Вычисляемая переменная displayables в протоколе View
возвращает нам все displayables
в виде массива. В SwiftUI
существует аналогичный способ получить все displayable
элементы из View
, но этот способ является приватным. Контейнерные View
, присутствующие в дереве макета, будут извлекать displayables
из обернутого ими View
. Сами контейнеры будут иметь массив displayables
из одного элемента.
Функция update
в нашем протоколе View
используется для сравнения: она сравнивает текущий View
с новым View
того же типа. Она должна уметь сообщать нам о том, какой View
изменился, чтобы мы могли обновить граф View
, и о том, как изменились displayables
, чтобы мы могли обновить контейнерные View
.
Мы используем параметр offset
в качестве индекса первого displayable
, который имеется в списке displayables
текущего контейнера. Нам это нужно, потому что View
, на котором вызывается update
, может не быть первым View
внутри своего контейнера.
Для составных View
мы предоставим стандартную реализацию displayables
и update
, которая перенаправляет вызовы к body
. SwiftUI
избегает лишних вызовов метода body
, и делает их только тогда, когда это необходимо. Но это лишь оптимизация и не влияет на алгоритм сравнения.
extension View {
var displayables: [Displayable] { body.displayables }
func update(using view: Self, offset: Int) {
body.update(using: view.body, offset: offset)
}
}
Чтобы примитивные View
работали с body
типа Never
, нам необходимо добавить поддержку протокола View
для него:
extension Never: View {
var body: some View {
fatalError()
}
}
Мы знаем, что displayable
является частью дерева макета и может генерировать graphic
. Но внутреннее устройство нашего протокола Displayable
сейчас не имеет значения для алгоритма вычисления разницы.
protocol Displayable {}
Используя наш собственный протокол View
, мы теперь переопределим несколько примитивных View
из SwiftUI
. Мы хотели бы сравнить два View
одного и того же типа и выяснить, какие View
добавлены или удалены из их графа View
, и какие displayables
были добавлены или удалены из их контейнера в дереве макета.
Мы также хотели бы увидеть, как типы View
помогают нам при их сравнении и почему их использование лучше, чем прямое сравнение displayables
типа Displayable
.
Вместе с созданием некоторых примитивных View
, мы также создадим ViewBuilder
, чтобы мы могли использовать тот же удобный синтаксис, который предоставляет SwiftUI
.
@resultBuilder
struct ViewBuilder {
// Единственная обязательная функция
static func buildBlock<Content: View>(_ content: Content) -> Content {
content
}
}
Позволим себе начать с создания одного унарного View
- Text
. Это представление будет отображаться через один displayable
, у таких View
размер примем за 1
:
struct Text: View {
let text: String
init(_ text: String) {
self.text = text
}
var body: Never { fatalError() }
}
extension Text {
var displayables: [Displayable] {
[TextDisplayable(text: text)]
}
}
struct TextDisplayable: Displayable {
let text: String
}
Найти то, что изменилось при обновлении, в методе update
достато просто. Строковое значение, используемое внутри Text
либо меняется, либо нет:
extension Text {
func update(using view: Text, offset: Int) {
if self.text != view.text {
print("Changed displayable at \(offset)")
}
}
}
Самый простой View
с размером больше 1
, вероятно, будет TupleView
. Как и в SwiftUI
, мы также создадим свой TupleView
: достаточно поместить два View
во ViewBuilder
. Пусть пока наш TupleView
поддерживает лишь кортежи длиной 2
:
struct TupleView<C0: View, C1: View>: View {
let content: (C0, C1)
var body: Never { fatalError() }
}
extension ViewBuilder {
static func buildBlock<C0: View, C1: View>(_ c0: C0, _ c1: C1) -> TupleView<C0, C1> {
TupleView(content: (c0, c1))
}
}
TupleView
с кортежем длиной 2
берет два других View
и комбинирует их displayables
:
extension TupleView {
var displayables: [Displayable] {
content.0.displayables + content.1.displayables
}
}
TupleView
фиксированы: внутри этого View
нет ничего, что могло бы измениться, только его дочерние элементы могут изменяться, поэтому функция update
будет выглядеть так:
extension TupleView {
func update(using view: TupleView<C0, C1>, offset: Int) {
content.0.update(using: view.content.0, offset: offset)
content.1.update(using: view.content.1, offset: offset + view.content.0.displayables.count)
}
}
Знаете ли вы, какой размер у TupleView
длиной 2
? Это каверзный вопрос, потому что он не обязательно равен 2
. Скорее, это сумма размеров комбинированных View
в этом кортеже. TupleView<Text, Text>
имеет размер 2
, но TupleView<TupleView<Text, Text>, Text>
имеет размер 3
.
Для более сложных View
создадим _ConditionalView
и функции ViewBuilder, которые позволят нам создавать их с помощью if-statement
выражений:
struct _ConditionalView<TrueContent: View, FalseContent: View>: View {
enum ConditionalContent {
case first(TrueContent)
case second(FalseContent)
}
let content: ConditionalContent
var body: Never { fatalError() }
}
extension ViewBuilder {
static func buildEither<TrueContent: View, FalseContent: View>(first: TrueContent) -> _ConditionalView<TrueContent, FalseContent> {
_ConditionalView(content: .first(first))
}
static func buildEither<TrueContent: View, FalseContent: View>(second: FalseContent) -> _ConditionalView<TrueContent, FalseContent> {
_ConditionalView(content: .second(second))
}
}
extension _ConditionalView {
var displayables: [Displayable] {
switch content {
case .first(let content):
return content.displayables
case .second(let content):
return content.displayables
}
}
}
_ConditionalView
более интересен, потому что это первый View
, в котором содержимое может быть добавлено или удалено с экрана. Если содержимое не изменилось с TrueContent
на FalseContent
или наоборот, то только дочерний элемент _ConditionalView
мог измениться, и нам нужно обновить его аналогично тому, как мы это делали для TupleView
. Но если содержимое изменилось, мы удаляем старые элементы с экрана и добавляем новые элементы:
extension _ConditionalView {
func update(using view: _ConditionalView<TrueContent, FalseContent>, offset: Int) {
switch (content, view.content) {
case (.first(let oldValue), .first(let newValue)):
// Содержимое не изменилось, но дочерний элемент мог измениться
oldValue.update(using: newValue, offset: offset)
case (.second(let oldValue), .second(let newValue)):
// Содержимое не изменилось, но дочерний элемент мог измениться
oldValue.update(using: newValue, offset: offset)
case (.first(let oldValue), .second(let newValue)):
// Содержимое было заменено!
for i in 0 ..< oldValue.displayables.count {
print("Removed displayable at \(offset + i)")
}
for i in 0 ..< newValue.displayables.count {
print("Inserted displayable at \(offset + i)")
}
case (.second(let oldValue), .first(let newValue)):
// Содержимое было заменено!
for i in 0 ..< oldValue.displayables.count {
print("Removed displayable at \(offset + i)")
}
for i in 0 ..< newValue.displayables.count {
print("Inserted displayable at \(offset + i)")
}
}
}
}
Мы можем написать структурные View
: Group
, Optional where Wrapped: View
и EmptyView
аналогичным образом. Мы рассмотрим ForEach
позже, но давайте испытаем то, что у нас уже есть!
struct TestList: View {
var value: Bool
@ViewBuilder
var body: some View {
Text("One")
if (value) {
Text("Two")
} else {
Text("Three")
}
}
}
let view1 = TestList(value: true)
print(view1.body.displayables)
[TextDisplayable(text: "One"), TextDisplayable(text: "Two")]
let view2 = TestList(value: false)
view1.body.update(using: view2.body, offset: 0)
Removed displayable at 1 Inserted displayable at 1
Написанный нами код выяснил, что View
был заменен, и мог определить, как displayables
должны измениться в контейнере, в котором мы находимся.
Давайте немного изменим эксперимент:
struct OtherTestList: View {
var value: Bool
@ViewBuilder
var body: some View {
Text("One")
Text(value ? "Two" : "Three")
}
}
let view1 = OtherTestList(value: true)
print(view1.body.displayables)
[TextDisplayable(text: "One"), TextDisplayable(text: "Two")]
let view2 = OtherTestList(value: false)
view1.body.update(using: view2.body, offset: 0)
Changed displayable at 1
Помните пример во введении, где было неясно, какие изменения произошли в списке меток? Это тот же пример! Но созданный нами алгоритм точно знает, изменился ли текст метки или она была заменена другой меткой. Он смог это сделать, используя статическую типизацию и позволяя самим типам View
определить, что изменилось.
Теперь мы знаем, как получить список displayables
из View
. Мы также знаем, что изменилось, если нам предоставлен View
того же типа. Контейнерный View
, такой как List
, будет читать displayables
из обернутого им View
и регистрироваться для получения уведомлений о изменениях этих displayables
. Вы можете видеть, как это можно использовать с UITableView
.
В протоколе View
в приведенном выше примере представления сравнивают другие экземпляры того же типа с самими собой. Таким образом, мы могли бы выяснить, какие представления были добавлены, удалены или изменены. Мы также могли бы узнать, что изменилось в текущем контейнерном представлении, и, следовательно, какие изменения произошли в дереве макета.
Но функция update
не имела ссылок на узлы графа представлений или на текущий контейнер, так что, помимо вывода того, что нужно изменить, мы фактически не можем применить изменения. Чтобы сделать внутренние функции протокола View
ближе к тем, которые могут реально работать, нам нужно добавить эти ссылки в код.
Мы можем добавить ссылку на текущий узел графа View
в качестве параметра функции update
. Таким образом, когда мы увидим, что представление вставляется или удаляется, мы также можем создать или удалить узел графа View
.
Ссылку на текущий контейнер можно добавить в узел графа View
. Когда мы видим, что View
было вставлено или удалено, мы сможем информировать текущий контейнер об изменении с учетом смещений соответствующих displayables
.
Узлы графа View
для унарных представлений также должны содержать отображаемый элемент: его можно будет изменить позже. Если строка внутри Text
меняется, нам нужно обновить отображаемый элемент. Если модификатор изменяется, нам нужно получить все отображаемые элементы ниже его узла в графе View
и обновить их.
Мы еще не обсуждали ForEach
, потому что, к сожалению, ForEach
отличается от других. Мы могли использовать типы, чтобы увидеть, как изменяются body
вложенных View
, но у нас нет способа сообщить о том, как именно изменились данные внутри ForEach
. Это означает, что мы не можем устранить неоднозначность таким же образом, как это делали для других структурных View
.
Но и в этом есть смысл: ведь именно поэтому ForEach
требует, чтобы данные в нём соответствовали Identifiable
. И это означает, что нам нужен алгоритм нахождения кратчайшего пути редактирования (shortest-edit-script algorithm
). Кажется, что SwiftUI
его не использует, но в Swift
даже есть встроенный алгоритм для выполнения такого сравнения.
Лень может не иметь прямого отношения к алгоритму сравнения в SwiftUI
, но мы уже изучили граф View
и контейнеры, и было бы обидно не обсудить очень интересное свойство ленивых View
. Кроме того, это даст нам подсказку о том, как сделать параметр offset
более эффективным.
Одно из ленивых контейнерных представлений в SwiftUI
— это List
. Это замечательное представление, так как оно использует внутри себя UITableView
из фреймворка UIKit
, с которым мы знаем, как работать. Вы даже можете проверить это, установив точки останова на методах insertRows
и removeRows
в UITableView
.
Хотя List
является ленивым, поскольку использует таблицу, он все же должен знать, сколько строк ему нужно будет отображать и каково смещение элементов, которые добавляются или удаляются. Это не очень хорошо сочетается с ленивостью. Можем ли мы узнать размер составного View
без его вычисления?
Оказывается, во многих случаях мы можем это сделать, просто зная тип body
составного View
. Например, размер TupleView<(Text, Text, Button)>
всегда равен 3
, независимо от конкретного экземпляра. Таким образом, составное View
, где тело — это TupleView<(Text, Text, Button)>
, также всегда имеет размер 3
.
Но вы не всегда можете узнать размер представления, просто зная его тип. _ConditionalView<EmptyView, Text>
может иметь размеры 0
или 1
. Если составное представление имеет тело такого типа и используется в ленивом стеке или списке, нам все равно придется оценивать его, чтобы определить его размер.
Для этого можем добавить статическую вычисляемую переменную size
к нашему протоколу View
. Составной View
перенаправит вызов к типу, возвращаемому в body
. Примитивные типы могут либо возвращать значение размера, если этот размер статичен, либо nil
, если размер динамический. Если возвратился nil
, значит мы не можем определить размер, просто взглянув на тип.
Вы можете проверить, что именно так это и работает в SwiftUI
. Допустим, имеется такое представление:
struct ContentView: some View {
var body: some View {
List {
ForEach(0 ..< 1000, id: \.self) { i in
ListItem(i)
}
}
}
}
Если ListItem
имеет body
статического, постоянного размера, как у Text
или TupleView<(Text, Text, Button)>
, то этот body
вызовется только для тех ListItem
, которые отображаются на экране в данный момент. Но если будет использован составной View
с динамическим размером body
, например, потому что использовалось if-statement
выражение внутри функции, то тогда body
будут вызваны немедленно у всех вложенных View
.
Мы исследовали, как статические типы представлений в SwiftUI
могут использоваться для эффективного и однозначного алгоритма сравнения. Если вы хотите узнать еще больше о том, как эту часть SwiftUI
можно было бы реализовать, изучите исходный код SwiftTUI.
Теперь стало возможным объяснить некоторое поведение SwiftUI
. Мы видим, почему невозможно переместить представление в другое место, переместив его в другого родителя в графе View
. Мы также видим, почему мы не можем добавлять или удалять модификаторы для представлений.
SwiftUI
кажется магией лишь потому, что он закрытый и его внутренние механизмы не описаны публично. Но, исследуя на практике конкретные примеры, мы можем узнать много его принципов. Используя возможности языка Swift, мы можем воссоздать многие части библиотеки. Я надеюсь, что это позволит вам полюбить этот фреймворк, лучше его понять и эффективнее использовать.