С iOS 16 в SwiftUI появился новый адаптивный контейнер макетов ViewThatFits, цель которого  —  найти из заданного набора представлений наиболее подходящее и использовать его.

Для большинства это простой и удобный контейнер. Но мы тщательно проанализируем его, включая детали правил, смысл идеального размера, примеры применения и многое другое. А в конце закрепим его понимание, создав свою версию ViewThatFits.

ViewThatFits

Определение

В официальной документации SwiftUI дано такое определение ViewThatFits:

Представление, адаптируемое к доступному пространству посредством первого подходящего дочернего представления.

public struct ViewThatFits<Content> : View where Content : View {
public init(in axes: Axis.Set = [.horizontal, .vertical], @ViewBuilder content: () -> Content)
}

Во ViewThatFits дочерние представления оцениваются в том порядке, в котором указаны в инициализаторе. Первым выбирается то, чей идеальный размер по ограниченным осям соответствует предлагаемому размеру. То есть представления указываются в порядке предпочтения  —  обычно от наибольшего к наименьшему.

Но это не всегда так, ведь представление по одной ограниченной оси подходить может, а по другой нет. По умолчанию ViewThatFits ограничивается и по горизонтальной, и по вертикальной осям.

Оценка и логика представления ViewThatFits

Из заданных представлений во ViewThatFits выбирается наиболее подходящее. Каковы же критерии и порядок оценки? И как она в итоге представляется?

  1. Сначала ViewThatFits получает используемое пространство, то есть предлагаемый в его родительском представлении размер.
  2. Порядок оценки основан на порядке в замыкании ViewBuilder: сверху вниз, по одному для каждого вложенного представления.
  3. ViewThatFits запрашивает у каждого вложенного представления его идеальный размер  —  требуемый размер, который возвращается на основе неуказываемого предлагаемого размера.
  4. Исходя из настройки ограниченной оси, идеальный размер вложенного представления сравнивается на выбранной ограниченной оси с предлагаемым размером родительского представления ViewThatFits.
  5. Если по всем заданным ограниченным осям идеальный размер не превышает предлагаемого, вложенное представление выбирается и оценка последующих вложенных представлений прекращается.
  6. Если ни одно из вложенных представлений не соответствует условию, выбирается последнее вложенное представление в замыкании.
  7. ViewThatFits в качестве собственного предлагаемого размера передает в выбранное вложенное представление предлагаемый размер родительского и получает требуемый размер этого вложенного представления, который соответствует явно заданному предлагаемому размеру.
  8. ViewThatFits в качестве собственного требуемого размера возвращает родительскому представлению полученный требуемый размер.

В итоге, какое вложенное представление отображать, выбирается во ViewThatFits исходя из:

  • доступного пространства для ViewThatFits, то есть предлагаемого размера из его родительского представления;
  • ограниченной оси, заданной во ViewThatFits;
  • идеального размера вложенных представлений на ограниченной оси;
  • порядка расположения вложенных представлений.

Любое изменение этих факторов сказывается на конечном результате.

Например, более ориентированный на разработчика язык для его описания  —  такой код:

ViewThatFits(in: .horizontal) {
Text("Hello Beautiful World")
Text("Hello World")
Text("Hi")
}

ViewThatFits выберет первое текстовое представление в его замыкании, отображаемое без переноса на другую строку в пределах заданной ширины.

Поэтому, когда мы помещаем этот код в разные контексты, итоговое вложенное представление, то есть выбранное вложенное представление, варьируется:

ViewThatFits(in: .horizontal) {
Text("Hello Beautiful World") // 100 < ширина < 200
Text("Hello World") // 20 < ширина < 100
Text("Hi") // 10 < ширина < 20
}
.border(.blue) // требуемый размер ViewThatFits
.frame(width:100)
.border(.red) // предлагаемый размер из родительского представления

Когда ширина всего 100, отображается Text("Hello World"), когда 200  —  Text("Hello Beautiful World"):

Добавим сложности, задав с помощью .frame(width:10) доступному размеру  —  предлагаемому размеру родительского представления ViewThatFits  —  значение 10.

Каким будет конечное отображение в зависимости от ширины текста, указанной в комментариях к коду?

ViewThatFits(in: .horizontal) {
Text("Hello Beautiful World") // 100 < ширина < 200
Text("Hello World") // 20 < ширина < 100
Text("Hi") // 10 < ширина < 20
}
.border(.blue) // требуемый размер ViewThatFits
.frame(width:10)
.border(.red) // предлагаемый размер из родительского представления

Исходной целью было выбрать текст под заданный размер без автоматического переноса на другую строку. Почему не получилось?

Сначала ViewThatFits сравнивает каждый текст отдельно и обнаруживает, что все его идеальные ширины в замыкании больше 10, поэтому выбирается последний Text("Hi").

Сейчас Text("Hi") получает только предлагаемый размер с шириной 10. Согласно правилу отображения текста по умолчанию  —  с переносом, когда он не помещается  —  для полного отображения Hi нужно две строки.

Из этого понятно, что самим ViewThatFits, когда он наконец представлен, ограничения по идеальному размеру на вложенные представления не накладываются. Идеальный размер вложенных представлений применяется им лишь для оценки на этапе проверки.

На этапе окончательного представления во вложенные представления им отправляется драгоценный предлагаемый размер, а требуемый их размер используется как собственный.

Чтобы справиться с этой экстремальной ситуацией переноса текста, сделаем для вложенных представлений специальные настройки.

Например, для принудительного отображения всего содержимого, когда размер при конечном отображении превышает предлагаемый размер родительского представления, применим fixedSize:

Text("Hi")
.fixedSize(horizontal: true, vertical: false)

Или с помощью lineLimit ограничимся одной строкой вертикального пространства, но полного отображения всего содержимого этим не гарантируется:

Text("Hi")
.lineLimit(1)

Я намеренно усложнил задачу. Чтобы использовать ViewThatFits корректно, нужно полностью понимать логику его оценки и отображения, а также концепцию «идеального размера». Иначе столкнемся с ситуациями, которые не соответствуют ожиданиям.

Идеальный размер

В SwiftUI многие разработчики идеальный размер знают и понимают не так хорошо, как предлагаемый.

С точки зрения макетов, «идеальным размером» называется возвращаемый представлением требуемый размер, в родительском представлении которого предлагается неуказываемый размер.

На более понятном языке идеальный размер  —  это размер, занимаемый идеальным результатом отображения представления, без ограничений по размеру последнего, то есть с идеальной внешней средой.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи fatbobman ( 东坡肘子): Mastering ViewThatFits

Предыдущая статьяGo — единственный выбор для бэкенд-разработчика?
Следующая статьяШардинг как паттерн архитектуры базы данных