На WWDC 2023 компания Apple представила модификатор представления containerRelativeFrame
для SwiftUI. Этот модификатор упрощает некоторые операции размещения элементов на экране, которые ранее было сложно выполнить обычными методами. В этой статье мы подробно рассмотрим модификатор containerRelativeFrame
, его определение, правила компоновки, примеры использования и важные соображения. Чтобы еще больше расширить наше понимание его функциональных возможностей, в конце статьи мы также создадим обратно совместимую реплику containerRelativeFrame
для старых версий SwiftUI.
Определение
В официальной документации Apple containerRelativeFrame описывается следующим образом:
Помещает представление в невидимый фрейм, размеры которого задаются относительно ближайшего контейнера.
Используйте этот модификатор, чтобы задать размер ширины и/или высоты представления в зависимости от размера ближайшего контейнера. В качестве такого контейнера могут выступать разные вещи, среди которых:
Окно, содержащее представление, на iPadOS или macOS, или экран устройства на iOS.
Колонка NavigationSplitView
NavigationStack
Вкладка TabView
Представление с возможностью прокрутки, например ScrollView или List
Размер, указанный в этом модификаторе, — это размер контейнера, подобного перечисленным выше, за вычетом любых вставок безопасной области, которые могут быть применены к этому контейнеру.
Помимо приведенного выше определения, официальная документация содержит несколько примеров кодов, помогающих разобраться с применением этого модификатора. Чтобы лучше прояснить механизм его работы, я повторно опишу функциональные возможности этого модификатора, основываясь на своем понимании:
Модификатор containerRelativeFrame
, начиная с представления, к которому он применяется, ищет в иерархии представлений ближайший контейнер, который вписывается в список допустимых контейнеров. Основываясь на правилах трансформации, заданных разработчиком, он вычисляет размер, полученный из найденного контейнера, и использует его для своего представления. В некотором смысле его можно рассматривать как специальную версию модификатора frame, которая позволяет задавать пользовательские правила трансформации.
Конструкторы
containerRelativeFrame
предлагает три вида конструкторов, каждый из которых отвечает различным потребностям в размещении элементов на экране:
1) Базовая версия: Используя этот конструктор, модификатор никак не преобразует размер контейнера. Вместо этого он напрямую принимает размер, полученный от ближайшего контейнера, в качестве размера для представления.
public func containerRelativeFrame(_ axes: Axis.Set, alignment: Alignment = .center) -> some View
2) Версия с предварительно заданными параметрами: С помощью этой версии разработчики могут указать разбиение размера на количество столбцов или строк, которые нужно охватить, и расстояние между ними, трансформируя соответствующим образом размер по заданным осям. Этот метод особенно подходит для настройки размера представления пропорционально размеру контейнера.
public func containerRelativeFrame(_ axes: Axis.Set, count: Int, span: Int = 1, spacing: CGFloat, alignment: Alignment = .center) -> some View
3) Полностью кастомизируемая версия: Этот конструктор обеспечивает максимальную гибкость, позволяя разработчикам задать собственную логику расчета в зависимости от размера контейнера. Он подходит для крайне специфических требований к макету.
public func containerRelativeFrame(_ axes: Axis.Set, alignment: Alignment = .center, _ length: @escaping (CGFloat, Axis) -> CGFloat) -> some View
Эти конструкторы предоставляют разработчикам мощные инструменты для создания сложных макетов, отвечающих различным требованиям к интерфейсу.
Пара слов о понятиях
Чтобы глубже понять функциональные возможности модификатора containerRelativeFrame
, мы тщательно разберем нескольких ключевых концепций, упомянутых в его определении.
Список контейнеров
В SwiftUI, как правило, дочерние представления напрямую получают размеры от родительских представлений. Однако, когда мы применяем к представлению модификатор frame
, дочернее представление игнорирует предложенный размер родительского представления и использует для указанной оси размеры из frame
.
VStack {
Rectangle()
.frame(width: 200, height: 200)
// остальные представления
...
}
.frame(width: 400, height: 500)
Например, при работе на iPhone, если мы хотим, чтобы высота Rectangle
была равна половине доступной высоты экрана, мы можем использовать следующую логику:
var screenAvailableHeight: CGFloat // Получаем доступную высоту экрана каким-либо способом
VStack {
Rectangle()
.frame(width: 200, height: screenHeight / 2)
// остальные представления
...
}
.frame(width: 400, height: 500)
До появления containerRelativeFrame
для получения размеров экрана приходилось использовать методы типа GeometryReader
или UIScreen.main.bounds
. Теперь мы можем добиться того же эффекта более удобным способом:
@main
struct containerRelativeFrameDemoApp: App {
var body: some Scene {
WindowGroup {
VStack {
Rectangle()
// Разделяем вертикальную ось на два и возвращаем
.containerRelativeFrame(.vertical){ height, _ in height / 2}
}
.frame(width: 400, height: 500)
}
}
}
Или
@main
struct containerRelativeFrameDemoApp: App {
var body: some Scene {
WindowGroup {
VStack {
Rectangle()
// Разделяем вертикальную ось на две равные части, занимаем одну, без промежутков
.containerRelativeFrame(.vertical, count: 2, span: 1, spacing: 0)
}
.frame(width: 400, height: 500)
}
}
}
В приведенном выше коде Rectangle()
игнорирует предложенный VStack
размер 400 x 500 и вместо этого ищет подходящий контейнер непосредственно сверху по иерархии представлений. В данном примере подходящим контейнером является экран iPhone.
Это означает, что containerRelativeFrame
предоставляет доступ к размерам контейнера в разных иерархиях представлений. Однако он может получить доступ только к размерам, предоставляемым конкретными контейнерами, перечисленными в списке допустимых контейнеров (таким как окно, ScrollView
, TabView
, NavigationStack
и т. д.).
Ближайшие контейнеры
Если в иерархии представлений сразу несколько контейнеров соответствуют критериям, containerRelativeFrame
выберет из них ближайший к текущему представлению. Например, в следующем фрагменте кода конечная высота Rectangle
равна 100, потому что используется высота NavigationStack
(200), деленная на 2, а не половина доступной высоты экрана.
@main
struct containerRelativeFrameDemoApp: App {
var body: some Scene {
WindowGroup {
NavigationStack {
VStack {
Rectangle() // высота равна 100
.containerRelativeFrame(.vertical) { height, _ in height / 2 }
}
.frame(width: 400, height: 500)
}
.frame(height: 200) // высота NavigationStack равна 200
}
}
}
Но будьте осторожны при использовании этого модификатора при разработке шаблонных представлений, так как один и тот же код может привести к различным результатам компоновки в зависимости от его местоположения.
Кроме того, необходимо соблюдать особую осторожность при использовании containerRelativeFrame
в overlay
или background
представлениях, которые попадают в список допустимых контейнеров. В таких случаях containerRelativeFrame
будет игнорировать текущий контейнер при поиске ближайшего контейнера. Это поведение отличается от типичного поведения overlay или background представлений.
Обычно считается, что представление и его overlay
и background
находятся в отношениях "master-slave". Чтобы узнать больше, читайте статью Разбираемся с модификаторами Overlay и Background в SwiftUI.
Ниже приведен пример, в котором к NavigationStack
применяется overlay
, содержащий Rectangle
, который использует для определения высоты containerRelativeFrame
. Здесь containerRelativeFrame
не будет использовать высоту NavigationStack
, а вместо этого будет искать размеры контейнера более высокого уровня — в данном случае это размер экрана.
@main
struct containerRelativeFrameDemoApp: App {
var body: some Scene {
WindowGroup {
NavigationStack {
VStack {
Rectangle()
}
.frame(width: 400, height: 500)
}
.frame(height: 200) // высота NavigationStack равна 200
.overlay(
Rectangle()
.containerRelativeFrame(.vertical) { height, _ in height / 2 } // доступная высота экрана / 2
)
}
}
}
Правила трансформации
Среди конструкторов, предлагаемых containerRelativeFrame
, есть два метода, которые позволяют динамически изменять размеры. Последний обеспечивает наибольшую гибкость:
public func containerRelativeFrame(_ axes: Axis.Set, alignment: Alignment = .center, _ length: @escaping (CGFloat, Axis) -> CGFloat) -> some View
Замыкание length
в этом методе применяется к двум разным осям, что позволяет рассчитывать размеры для каждой оси отдельно. Например, в следующем коде ширина Rectangle
устанавливается равной двум третям доступной ширины ближайшего контейнера, а высота — половине доступной высоты:
Rectangle()
.containerRelativeFrame([.horizontal, .vertical]) { length, axis in
if axis == .vertical {
return length / 2
} else {
return length * (2 / 3)
}
}
Для осей, не указанных в параметре конструктора axes
, containerRelativeFrame
не будет устанавливать размеры (сохранятся предложенные размеры, заданные родительским представлением).
struct TransformsDemo: View {
var body: some View {
VStack {
Rectangle()
.containerRelativeFrame(.horizontal) { length, axis in
if axis == .vertical {
return length / 2 // Эта строка не будет выполнена, потому что .vertical не задана в axes
} else {
return length * (2 / 3)
}
}
}.frame(height: 100)
}
}
В приведенном выше коде ширина Rectangle
устанавливается равной двум третям доступной ширины ближайшего контейнера, а высота остается равной 100 (что соответствует высоте родительского VStack
).
Подробное объяснение второго конструктора будет рассмотрено в следующем разделе.
Размер, предоставляемый контейнером
В официальной документации размер, используемый модификатором containerRelativeFrame
, описывается следующим образом: "Размер, предоставляемый этому модификатору, — это размер контейнера за вычетом любых вставок безопасной области, которые могут быть применены к этому контейнеру". Это описание в принципе верно, но есть несколько важных деталей, на которые следует обратить внимание при его реализации с различными контейнерами:
При использовании в
NavigationSplitView
containerRelativeFrame
получает размеры текущей колонки (SideBar
,Content
,Detail
). Помимо учета уменьшения за счет безопасных областей, в верхней области также должна быть вычтена высота панели инструментов (navigationBarHeight
). Однако при использовании вNavigationStack
высота панели инструментов не вычитается.При использовании
containerRelativeFrame
вTabView
вычисляемая высота — это общая высотаTabView
минус высота безопасной области вверху иTabBar
внизу.В
ScrollView
, если разработчик добавилpadding
черезsafeAreaPadding
, тоcontainerRelativeFrame
также вычтет значения заполнения.В средах, поддерживающих несколько окон (iPadOS, macOS), размер корневого контейнера соответствует доступным размерам окна, в котором в данный момент отображается представление.
Хотя в официальной документации указано, что
containerRelativeFrame
можно использовать сList
, по факту в Xcode версии 15.3 (15E204a) этот модификатор пока не способен корректно вычислять размеры списка.
Примеры использования
Освоив принципы работы модификатора containerRelativeFrame
, вы можете использовать его для выполнения многих задач верстки, которые ранее были невозможны или трудновыполнимы. В этом разделе мы продемонстрируем несколько показательных примеров.
Создание галерей, пропорциональных размерам области прокрутки
Это распространенный сценарий, который часто упоминается в статьях, посвященных использованию containerRelativeFrame
. Рассмотрим следующую задачу: нам нужно создать горизонтально прокручиваемый макет галереи, похожий то, что мы видим в App Store или Apple Music, где каждое дочернее представление (изображение) занимает одну треть ширины прокручиваемой области и две трети высоты от ее ширины.
Обычно, если не используется containerRelativeFrame
, разработчики могут использовать метод, представленный в руководстве SwiftUI geometryGroup(): От теории к практике, который включает в себя добавление background‘а к ScrollView
для получения его размеров, а затем передачу каким-либо образом этой информации для установки конкретных размеров дочерних представлений. Это означает, что мы не можем добиться этого только за счет манипуляций с дочерними представлениями, сначала мы должны получить размеры ScrollView
.
Эту задачу можно легко выполнить, используя второй конструктор containerRelativeFrame
,:
struct ScrollViewDemo:View {
var body: some View {
ScrollView(.horizontal) {
HStack(spacing: 10) {
ForEach(0..<10){ _ in
Rectangle()
.fill(.purple)
.aspectRatio(3 / 2, contentMode: .fit)
// Горизонтально делим на три части, занимаем одну, без интервалов
.containerRelativeFrame(.horizontal, count: 3, span: 1, spacing: 0)
}
}
}
}
}
Внимательные читатели могут заметить, что, поскольку сам HStack
имеет spacing: 10
, третье представление (крайнее справа) будет отображаться не полностью — небольшая часть не влезет в области прокрутки. Если вы хотите учитывать интервал в HStack при установке ширины дочерних представлений, то вам нужно будет указать его в настройке spacing
containerRelativeFrame
. Благодаря этой настройке ширина каждого дочернего представления будет чуть меньше одной трети ширины видимой области ScrollView
с учетом расстояния между ними, и мы сможем наблюдать все три представления полностью на начальном экране.
struct ScrollViewDemo:View {
var body: some View {
ScrollView(.horizontal) {
HStack(spacing: 10) {
ForEach(0..<10){ _ in
Rectangle()
.fill(.purple)
.aspectRatio(3 / 2, contentMode: .fit)
.border(.yellow, width: 3)
// Учет расстояний в расчетах
.containerRelativeFrame(.horizontal, count: 3, span: 1, spacing: 10)
}
}
}
}
}
Параметр spacing
в containerRelativeFrame
отличается от параметра spacing
в таких контейнерах, как VStack
и HStack
. Он не добавляет пространство напрямую, а используется во втором варианте конструкторе в качестве коэффициента в правилах трансформации.
В официальной документации роль count
, span
и spacing
в правилах трансформации объясняется на примере вычисления ширины:
let availableWidth = (containerWidth - (spacing * (count - 1)))
let columnWidth = (availableWidth / count)
let itemWidth = (columnWidth * span) + ((span - 1) * spacing)
Важно отметить, что из-за особенностей компоновки ScrollView
(в направлении прокрутки он использует все предлагаемые размеры, а в направлении без прокрутки зависит от требуемых размеров дочерних представлений) при использовании containerRelativeFrame
в ScrollView
параметр axes
должен как минимум включать обработку размеров в направлении прокрутки (если дочерние представления не указали конкретные требуемые размеры). В противном случае это может привести к аномальному поведению. Например, следующий код в большинстве случаев будет вызывать краш приложения:
struct ScrollViewDemo:View {
var body: some View {
ScrollView {
HStack(spacing: 10) {
ForEach(0..<10){ _ in
Rectangle()
.fill(.purple)
.aspectRatio(3 / 2, contentMode: .fit)
.border(.yellow, width: 3)
// вычисляемое направление не совпадает с направлением прокрутки
.containerRelativeFrame(.horizontal, count: 3, span: 1, spacing: 0)
}
}
}
.border(.red)
}
}
Примечание: Из-за различий в логике компоновки между LazyHStack
и HStack
использование LazyHStack
вместо HStack
может привести к тому, что ScrollView будет занимать все доступное пространство, что может не соответствовать ожидаемой компоновке (в официальных примерах документации используется LazyHStack
). В сценариях, где LazyHStack
необходим, лучшим выбором может быть использование GeometryReader
для получения ширины ScrollView
и вычисления результирующей высоты, чтобы макет соответствовал ожиданиям.
Установка пропорциональных размеров
Когда требуемые пропорции размеров неравномерны, больше подходит третий вид конструктора, позволяющий полностью кастомизировать правила трансформации. Рассмотрим следующий сценарий: нам нужно отобразить фрагмент текста внутри контейнера (например, NavigationStack
или TabView
) и задать фон, состоящий из двух цветов — синего сверху и оранжевого снизу, с разделением на золотом сечении контейнера (0.618).
Не используя containerRelativeFrame
, мы могли бы реализовать это следующим образом:
struct SplitDemo:View {
var body: some View {
NavigationStack {
ZStack {
Color.blue
.overlay(
GeometryReader { proxy in
Color.clear
.overlay(alignment: .bottom) {
Color.orange
.frame(height: proxy.size.height * (1 - 0.618))
}
}
)
Text("Hello World")
.font(.title)
.foregroundStyle(.yellow)
}
}
}
}
С containerRelativeFrame
наша логика будет совершенно другой:
struct SplitDemo: View {
var body: some View {
NavigationStack {
Text("Hello World")
.font(.title)
.foregroundStyle(.yellow)
.background(
Color.blue
// Синий цвет занимает все свободное пространство контейнера
.containerRelativeFrame([.horizontal, .vertical])
.overlay(alignment: .bottom) {
Color.orange
// Высота оранжевого цвета — это высота контейнера умноженная на (1 - 0.618), выровненная по низу синего цвета
.containerRelativeFrame(.vertical) { length, _ in
length * (1 - 0.618)
}
}
)
}
}
}
Если вы хотите, чтобы синий и оранжевый фоны выходили за пределы безопасной области, вы можете добиться этого, добавив модификатор ignoresSafeArea
:
NavigationStack {
Text("Hello World")
.font(.title)
.foregroundStyle(.yellow)
.background(
Color.blue
.ignoresSafeArea()
.containerRelativeFrame([.horizontal, .vertical])
.overlay(alignment: .bottom) {
Color.orange
.ignoresSafeArea()
.containerRelativeFrame(.vertical) { length, _ in
length * (1 - 0.618)
}
}
)
}
В статье GeometryReader: Дар или проклятие? мы рассмотрели, как использовать GeometryReader
для размещения двух представлений в определенном соотношении в заданном пространстве. Хотя containerRelativeFrame
поддерживает получение размеров только из конкретного списка контейнеров, мы все равно можем использовать определенные техники для удовлетворения аналогичных требований к расположению.
Вот пример реализации этого с помощью GeometryReader
:
struct RatioSplitHStack<L, R>: View where L: View, R: View {
let leftWidthRatio: CGFloat
let leftContent: L
let rightContent: R
init(leftWidthRatio: CGFloat, @ViewBuilder leftContent: @escaping () -> L, @ViewBuilder rightContent: @escaping () -> R) {
self.leftWidthRatio = leftWidthRatio
self.leftContent = leftContent()
self.rightContent = rightContent()
}
var body: some View {
GeometryReader { proxy in
HStack(spacing: 0) {
Color.clear
.frame(width: proxy.size.width * leftWidthRatio)
.overlay(leftContent)
Color.clear
.overlay(rightContent)
}
}
}
}
В версии, использующей containerRelativeFrame
, для предоставления размеров без включения функции прокрутки мы можем использовать ScrollView
:
struct RatioSplitHStack<L, R>: View where L: View, R: View {
let leftWidthRatio: CGFloat
let leftContent: L
let rightContent: R
init(leftWidthRatio: CGFloat, @ViewBuilder leftContent: @escaping () -> L, @ViewBuilder rightContent: @escaping () -> R) {
self.leftWidthRatio = leftWidthRatio
self.leftContent = leftContent()
self.rightContent = rightContent()
}
var body: some View {
ScrollView(.horizontal) {
HStack(spacing: 0) {
Color.clear
.containerRelativeFrame(.horizontal) { length, _ in length * leftWidthRatio }
.overlay(leftContent)
Color
.clear
.overlay(rightContent)
.containerRelativeFrame(.horizontal) { length, _ in length * (1 - leftWidthRatio) }
}
}
.scrollDisabled(true) // Используем ScrollView исключительно для получения размеров, прокрутка отключена
}
}
struct RatioSplitHStackDemo: View {
var body: some View {
RatioSplitHStack(leftWidthRatio: 0.25) {
Rectangle().fill(.red)
} rightContent: {
Color.clear
.overlay(
Text("Hello World")
)
}
.border(.blue)
.frame(width: 300, height: 60)
}
}
Получение размеров контейнера для вложенных представлений
Важной особенностью containerRelativeFrame
является его способность напрямую получать доступные размеры ближайшего подходящего контейнера в иерархии представления. Эта возможность особенно удобна для создания вложенных представлений или модификаторов представлений, которые содержат независимую логику и должны знать размеры своих контейнеров, но не хотят нарушать текущую компоновку представления, как это может сделать GeometryReader
.
Следующий пример демонстрирует, как создать ViewModifier
под названием ContainerSizeGetter
, назначение которого — получать и передавать доступные размеры своего контейнера (который является частью списка):
// Сохраняем полученные размеры, чтобы предотвратить их обновление во время цикла обновления представления
class ContainerSize {
var width: CGFloat? {
didSet {
sendSize()
}
}
var height: CGFloat? {
didSet {
sendSize()
}
}
func sendSize() {
if let width = width, let height = height {
publisher.send(.init(width: width, height: height))
}
}
var publisher = PassthroughSubject<CGSize, Never>()
}
// Получаем и передаем доступные размеры ближайшего контейнера
struct ContainerSizeGetter: ViewModifier {
@Binding var size: CGSize?
@State var containerSize = ContainerSize()
func body(content: Content) -> some View {
content
.overlay(
Color.yellow
.containerRelativeFrame([.vertical, .horizontal]) { length, axes in
if axes == .vertical {
containerSize.height = length
} else {
containerSize.width = length
}
return 0
}
)
.onReceive(containerSize.publisher) { size in
self.size = size
}
}
}
extension View {
func containerSizeGetter(size: Binding<CGSize?>) -> some View {
modifier(ContainerSizeGetter(size: size))
}
}
Этот ViewModifier
использует containerRelativeFrame
для измерения и обновления размеров контейнера и PassthroughSubject
для уведомления внешнего привязанного свойства size
о любых изменениях размеров. Преимущество этого метода в том, что он не нарушает исходный макет представления, служа лишь инструментом для контроля и передачи размеров.
Реплика containerRelativeFrame
В своих статьях, посвященных компоновке, я часто пытаюсь создать реплики контейнеров компоновки. Такая практика не только помогает глубже понять механизмы компоновки контейнеров, но и позволяет проверить гипотезы о некоторых аспектах логики. Кроме того, там, где это возможно, эти реплики могут быть применены к более ранним версиям SwiftUI (например, iOS 13+).
Чтобы упростить работу с репликацией в этой статье, текущая версия поддерживает только iOS. Полный код можно посмотреть здесь.
Определение ближайшего контейнера
Официальный containerRelativeFrame
может получить размеры ближайшего контейнера одним из двух способов:
Позволяя контейнерам передавать свои размеры вниз по иерархии.
Позволяя
containerRelativeFrame
автономно искать ближайший контейнер вверх по иерархии и получать его размеры.
Учитывая, что первый метод может увеличить нагрузку на систему (поскольку контейнеры будут постоянно отправлять изменения размеров, даже если containerRelativeFrame
не используется) и что сложно разработать точную логику передачи размеров для разных контейнеров, для нашей реплики мы выберем второй метод — автоматический поиск ближайшего контейнера вверх по иерархии.
extension UIView {
fileprivate func findRelevantContainer() -> (container: UIResponder, type: ContainerType)? {
var responder: UIResponder? = this
while let currentResponder = responder {
if let viewController = currentResponder as? UIViewController {
if viewController is UITabBarController {
return (viewController, .tabview) // UITabBarController
}
if viewController is UINavigationController {
return (viewController, .navigator) // UINavigationController
}
}
if let scrollView = currentResponder as? UIScrollView {
return (scrollView, .scrollView) // UIScrollView
}
responder = currentResponder.next
}
if let currentWindow = UIApplication.shared.windows.filter({$0.isKeyWindow}).first {
return (currentWindow, .window) // UIWindow
} else {
return nil
}
}
}
Добавив метод расширения findRelevantContainer
к UIView
, мы можем определить конкретный контейнер (возвращающий тип UIResponder
), который находится ближе всего к текущему представлению (UIView
).
Расчет размеров, получаемых от контейнера
После определения ближайшего контейнера необходимо настроить вставки безопасной области, высоту TabBar
, высоту NavigationBarHeight
и другие размеры в зависимости от типа контейнера. Это делается путем отслеживания изменений в свойстве frame
, чтобы динамически реагировать на изменения размеров:
@MainActor
class Coordinator: ObservableObject {
weak var container: UIResponder?
var type: ContainerType?
var size: Binding<CGSize?>
var cancellables = Set<AnyCancellable>()
init(size: Binding<CGSize?>) {
self.size = size
}
func trackContainerSizeChanges(_ container: UIResponder, ofType type: ContainerType) {
self.container = container
self.type = type
switch type {
case .window:
if let window = container as? UIWindow {
window.publisher(for: \.frame)
.receive(on: RunLoop.main)
.sink(receiveValue: { _ in
let size = self.calculateContainerSize(container, ofType: type)
self.size.wrappedValue = size
})
.store(in: &cancellables)
}
....
}
func calculateContainerSize(_ container: UIResponder, ofType type: ContainerType) -> CGSize? {
switch type {
case .window:
if let window = container as? UIWindow {
let windowSize = window.frame.size
let safeAreaInsets = window.safeAreaInsets
let width = windowSize.width - safeAreaInsets.left - safeAreaInsets.right
let height = windowSize.height - safeAreaInsets.top - safeAreaInsets.bottom
return CGSize(width: width, height: height)
}
....
return nil
}
}
}
Создание ViewModifier
Мы инкапсулируем описанную выше логику в представление SwiftUI с помощью UIViewRepresentable
и применяем ее к представлению, в конечном итоге используя модификатор frame
для применения преобразованных размеров к представлению:
private struct ContainerDetectorModifier: ViewModifier {
let type: DetectorType
@State private var containerSize: CGSize?
func body(content: Content) -> some View {
content
.background(
ContainerDetector(size: $containerSize)
)
.frame(width: result.width, height: result.height, alignment: result.alignment)
}
...
}
Проделав все вышеописанные операции, мы получили версию реплики, которая соответствует по своему функционалу официальному containerRelativeFrame
, и можем проверяем гипотезы о деталях реализации, не упомянутых в официальной документации.
Результаты показывают, что containerRelativeFrame
действительно можно рассматривать как специальную версию модификатора frame
, позволяющую использовать кастомные правила трансформации. Поэтому в этой статье я не стал уделять внимание использованию параметра alignment
, так как он полностью соответствует логике frame
.
Соображения:
На версиях iOS ниже 17, если реплика изменяет размеры по двум осям одновременно,
ScrollView
может вести себя некорректно.По сравнению с официальной версией, реплика производит более точное извлечение размеров для
List
.
Заключение
Благодаря подробному разбору и примерам, представленным в этой статье, вы должны иметь полное представление о модификаторе containerRelativeFrame
в SwiftUI, включая его определение, применение и рекомендации. Мы не только научились использовать этот мощный модификатор для оптимизации и инновации наших стратегий компоновки, но и узнали, как расширить его применение на старые версии SwiftUI, воспроизведя существующий инструмент, тем самым улучшив наше понимание механизмов компоновки SwiftUI. Я надеюсь, что этот материал пробудит ваш интерес к верстке в SwiftUI и окажется полезным для вас в разработке.
26 декабря пройдет открытый урок, посвященный теме навигации на SwiftUI без UIKit. На нем:
Разберем навигацию в проектах на SwiftUI.
Научимся писать приложение с нативной навигацией на SwiftUI с поддержкой iOS 14, используя OpenSource-решения и авторские разработки без UIKit.
Разберем интеграцию диплинков в проект в декларативном стиле.
Записаться на урок можно на странице курса "iOS Developer. Professional".