На 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».
ссылка на оригинал статьи https://habr.com/ru/articles/869346/
Добавить комментарий