SA0706 — Представляем Stacks & Containers
Содержание страницы
В предыдущей главе вы узнали об общих элементах управления SwiftUI, включая TextField
, Button
, Slider
и Toggle
. В этой главе вы познакомитесь с контейнерными представлениями, которые используются для группировки связанных представлений, а также для их размещения относительно друг друга.
Однако прежде чем начать, важно изучить и понять, как определяются размеры представлений.
Подготовка проекта
Прежде чем перейти к представлениям и их размерам, имейте в виду, что начальный проект этой главы имеет некоторые дополнения по сравнению с окончательным проектом предыдущей главы.
Если вы хотите продолжать работать над своей собственной копией, не волнуйтесь! Просто скопируйте эти файлы и добавьте в свой проект или перетащите их непосредственно в Xcode.
- Практика/ChallengeView.swift
- Практика/ChallengesViewModel.swift
- Практика/ChoicesRow.swift
- Практика/ChoicesView.swift
- Практика/CongratulationsView.swift
- Практика/PracticeView.swift
- Практика/QuestionView.swift
- StarterView.swift
- HistoryView.swift
Расположение и приоритеты
В UIKit и AppKit вы привыкли использовать автоматическую компоновку для ограничения представлений. Общее правило состояло в том, чтобы позволить родителю определять размер своих потомков, обычно получаемый путем добавления ограничений, если только их размер не был статически установлен с использованием, например, ограничений ширины и высоты.
Если провести сравнение с семейной моделью, то автоматическая компоновка-это консервативная модель, или патриархальная для обоих родителей, если хотите.
SwiftUI работает противоположно: дети выбирают свой размер в ответ на размер, предложенный родителем. Это скорее современная семейная модель — если у вас есть дети, вы понимаете, что я имею в виду!

Ожидание размера
Если у вас есть a Text
, и вы помещаете его в a View
, Text
то при рендеринге вида ему присваивается предлагаемый размер, соответствующий размеру родительского кадра. Однако Text
он вычислит размер отображаемого текста и выберет размер, необходимый для размещения этого текста, плюс дополнительные отступы, если таковые имеются.
Макет для представлений с одним дочерним элементом
Откройте стартовый проект и перейдите в раздел Practice/ChallengeView.swift, который представляет собой новое представление, созданное на основе шаблона представления SwiftUI. Вы можете видеть, что он содержит одинText
:
struct ChallengeView: View { var body: some View { Text("Hello World!") } }
Если вы повторно активируете предварительный просмотр в Xcode, вы увидите текст, отображаемый в центре экрана.
Примечание: По умолчанию каждое представление располагается в центре родительского представления.

Пустой Привет, Мир
Этот скриншот не дает никаких указаний на размер кадра текста. Попробуйте добавить красный фон:
Text("Hello World!") .background(Color.red)

Привет, Мир 1
Теперь вы можете видеть, что сами Text
размеры с минимумом содержат текст, который он отображает. Измените текст на “Большой и теплый прием в Кучи”.:
Text("A great and warm welcome to Kuchi") .background(Color.red)
Вы увидите, что Text
размер кадра изменяется в соответствии с новым содержимым.

Привет, Мир 2
Правила, которые SwiftUI применяет для определения размера родительского и дочернего представлений, следующие::
- Родительское представление определяет доступный в его распоряжении фрейм.
- Родительское представление предлагает размер дочернему представлению.
- На основе предложения от родителя дочернее представление выбирает свой размер.
- Родительское представление имеет такие размеры, что оно содержит свое дочернее представление.
Этот процесс является рекурсивным, начиная с корневого представления и вплоть до последнего листового представления в иерархии представлений.
Примечание: Каждый модификатор, примененный к представлению, создает новое представление, которое встраивает исходное представление. Описанный выше набор правил применяется ко всем представлениям, независимо от того, являются ли они отдельными компонентами или представлениями, генерируемыми модификаторами.
Чтобы увидеть это в действии, попробуйте указать фиксированный кадр Text
и новый цвет фона:
Text("A great and warm welcome to Kuchi") .background(Color.red) // fixed frame size .frame(width: 150, height: 50, alignment: .center) .background(Color.yellow)

Привет, Мир 3
Интересно, что вы можете видетьText
, что размер отличается от размера представления, созданного .frame
модификатором. Это не должно вас удивлять, потому что здесь применяются четыре правила, описанные выше:
- Вид кадра имеет фиксированный размер 150×50 точек.
- Вид кадра предлагает этот размер для
Text
. - Он
Text
находит способ отобразить текст в пределах этого размера, но с использованием минимума без необходимости усечения (когда это возможно).
Правило 4 пропускается, так как вид кадра уже имеет определенный размер. Он Text
автоматически упорядочивает текст для отображения в две строки, потому что понимает, что он не помещается в одну строку максимум из 150 точек без усечения.
Если вы увеличите размер кадра, то получите дополнительное доказательство того, как представления определяют свой размер. Попробуйте, например, больший размер 300×100:
.frame(width: 300, height: 100, alignment: .center)

Привет, Мир 4
Теперь Text
в его распоряжении достаточно ширины, чтобы отобразить текст в одну строку. Однако он по-прежнему занимает точное пространство, необходимое для рендеринга текста (на красном фоне), в то время как вид кадра использует фиксированный размер кадра (на желтом фоне).
Можете ли вы догадаться, что произойдет, если размер родительского представления окажется недостаточным для размещения дочернего представления? В случае a Text
он просто усечет текст. Попробуйте уменьшить размер кадра до 100х50:
.frame(width: 100, height: 50, alignment: .center)

Привет, Мир 5
Это происходит при отсутствии других условий, таких как использование .minimumScaleFactor
модификатора, который при необходимости заставляет текст сжиматься до коэффициента масштабирования, передаваемого в качестве параметра, который представляет собой значение от 0 до 1:
Text("A great and warm welcome to Kuchi") .background(Color.red) .frame(width: 100, height: 50, alignment: .center) // Add this scale factor .minimumScaleFactor(0.5) .background(Color.yellow)

Привет, Мир 6
Вообще говоря, компонент всегда будет пытаться уместить содержимое в пределах размера, предложенного его родителем. Если компонент не может этого сделать из-за того, что ему требуется больше места, он будет применять правила, соответствующие типу компонента и строго зависящие от него.
Это подкрепляет концепцию о том, что в SwiftUI каждый вид выбирает свой собственный размер. Он рассматривает предложения, сделанные его родителем, и пытается приспособиться к этому предложению в меру своих возможностей, но это всегда зависит от того, к какому типу компонентов относится представление.
Возьмем, к примеру, образ. При отсутствии других ограничений он будет отображаться в исходном разрешении, как вы можете видеть, если замените Text
компонент наImage
:
Image("welcome-background") .background(Color.red) .frame(width: 100, height: 50, alignment: .center) .background(Color.yellow)
Это то же самое изображение, которое вы использовали в главе 5: “Введение в элементы управления: текст и изображение”.

Привет, Мир 7
Красная стрелка выделяет статический кадр 100×50, но вы можете видеть, что изображение было отрисовано с его собственным разрешением, полностью игнорируя предлагаемый размер — по крайней мере, в отсутствие каких-либо других ограничений, таких как .resizable
модификатор, который позволил бы автоматически масштабировать изображение вверх или вниз по порядку чтобы занять все доступное пространство, предлагаемое его родителем:
Image("welcome-background") .resizable()

Привет, Мир 8
Итак, в конце концов, вы понимаете, что родитель не может навязать ребенку размер. Что может сделать родитель, так это предложить размер и в конечном итоге ограничить ребенка рамками по его выбору, но это не влияет на способность ребенка выбирать размер меньше или больше.
Некоторые компоненты, например Text
, будут пытаться быть адаптивными, выбирая размер, который наилучшим образом соответствует размеру, предложенному родителем, но все же с оглядкой на размер текста для рендеринга. Другие компоненты , напримерImage
, будут просто игнорировать предлагаемый размер.
В середине есть представления, которые более или менее адаптивны, но также и нейтральны, а это означает, что у них нет никаких причин выбирать размер. Они просто передадут это решение своим собственным детям и измерят себя так, чтобы просто обернуть своих детей.
Примером может служить .padding
модификатор, который не имеет внутреннего размера — он просто берет размер дочернего элемента, добавляет указанное заполнение к каждому из четырех ребер (верхнему, левому, правому, нижнему) и использует его для создания представления, которое встраивает дочерний элемент.
Представления стека
Вы использовали представления стека в предыдущих главах, но еще не изучали представления контейнеров достаточно глубоко. В следующем разделе мы рассмотрим более подробно и научим вас логике, лежащей в основе представлений.
Макет для контейнерных представлений
В случае контейнерного представления, т. Е. представления, содержащего два или более дочерних представления, правила, определяющие размеры дочерних представлений, следующие::
- Представление контейнера определяет доступный в его распоряжении фрейм, который обычно представляет собой размер, предложенный родителем.
- Представление контейнера выбирает дочернее представление с наиболее ограничительными ограничениями или, в случае эквивалентных ограничений, с наименьшим размером.
- Представление контейнера предлагает размер дочернему представлению. Предлагаемый размер — это доступный размер, деленный поровну на количество (оставшихся) дочерних просмотров.
- Дочернее представление, основываясь на предложении родителя, выбирает его размер.
- Представление контейнера вычитает из доступного кадра размер, выбранный дочерним представлением, и возвращается к шагу № 2, пока не будут обработаны все дочерние представления.
Различия между этим и случаем представлений с одним дочерним элементом, которые вы видели в предыдущем разделе, выделены жирным шрифтом.
Вернемся к кодексу! Восстановите Text
его таким, каким он был до того, как вы заменили его изображением, и продублируйте его внутриHStack
:
HStack { Text("A great and warm welcome to Kuchi") .background(Color.red) Text("A great and warm welcome to Kuchi") .background(Color.red) } .background(Color.yellow)
Вы уже сталкивались HStack
с этим в предыдущих главах, поэтому вам следует знать, что он раскладывает свои дочерние представления горизонтально. Поскольку эти два ребенка равны, вы можете ожидать, что они имеют одинаковый размер. Но это то, что вы получаете вместо этого (убедитесь, что не используете iPhone Pro Max для предварительного просмотра этого контента).:

Привет, Мир 9
Почему это так? Здесь необходима пошаговая разбивка:
- Стек получает предложенный размер от своего родителя и делит его на две равные части.
- Стек предлагает первый размер одному из детей. Они равны, поэтому он посылает предложение первому ребенку, тому, что слева.
- Он
Text
обнаруживает, что ему нужно меньше предлагаемого размера, потому что он должен отображать текст в двух строках и может отформатировать его таким образом, чтобы две строки имели одинаковую длину. - Стек вычитает размер, взятый первым
Text
, и предлагает полученный размер второмуText
. - Тот
Text
решает использовать все предложенные размеры.
Теперь попробуйте сделать второе Text
чуть меньше, заменив an m
на an n
, например, в словеwarm
:
Text("A great and warm welcome to Kuchi") .background(Color.red) Text("A great and warn welcome to Kuchi") // <- Replace `m` with // `n` in `warm` .background(Color.red)
Теперь, когда он меньше, второе Text
имеет приоритет; на самом деле, это первый размер, который будет предложен. Результирующий макет выглядит следующим образом:

Привет, Мир 10
Если хотите, вы можете поэкспериментировать с разницей между более длинными и сильными текстами в двух Text
элементах управления.
Приоритет компоновки
Представление контейнера сортирует своих дочерних элементов по степени ограничения, переходя от элемента управления с наиболее ограничительными ограничениями к элементу управления с наименьшими ограничениями. В случае, если ограничения эквивалентны, наименьшее будет иметь приоритет.
Однако бывают случаи, когда вам захочется изменить этот порядок. Это может быть достигнуто двумя различными способами, обычно для разных целей:
- Измените поведение представления с помощью модификатора.
- Измените приоритет макета представления.
Модификатор
Вы можете использовать модификатор, чтобы сделать представление более или менее адаптивным. Примеры включают:
Image
является одним из наименее адаптивных компонентов, поскольку игнорирует размер, предложенный его родителем. Но его поведение резко меняется после примененияresizable
модификатора, что позволяет ему слепо принимать любой размер, предложенный родителем.Text
он очень адаптивен, так как пытается отформатировать и обернуть текст так, чтобы наилучшим образом соответствовать предлагаемому размеру. Но он становится менее адаптивным, когда вынужден использовать максимальное количество строк с помощьюlineLimit
модификатора.
Изменения степени адаптивности непосредственно влияют на вес элемента управления в порядке сортировки.
Приоритет
У вас также есть возможность изменить приоритет макета с помощью .layoutPriority
модификатора. Таким образом, вы можете явно изменить вес элемента управления в порядке сортировки. Он принимает Double
значение, которое может быть как положительным, так и отрицательным. Можно предположить, что представление без явного приоритета компоновки имеет значение, равное нулю.
Вернитесь к файлу ChallengeView и замените содержимое представления стеком из трех текстовых копий:
`HStack {
Text(«A great and warm welcome to Kuchi»)
.background(Color.red)
Text(«A great and warm welcome to Kuchi»)
.background(Color.red)
Text(«A great and warm welcome to Kuchi»)
.background(Color.red)
}
.background(Color.yellow)`

Привет, Мир 11
Теперь попробуйте несколько четких приоритетов. При установке приоритетов можно использовать любую шкалу; например, ограничиться значениями в диапазоне [0, 1] или [-1, +1], использовать только целочисленные значения и так далее.
Важно то, что Stack
процессы начинаются от абсолютного максимума до абсолютного минимума. Если абсолютное наименьшее значение ниже нуля, представления без явного приоритета обрабатываются раньше всех представлений с отрицательным значением.
Добавьте приоритет макета 1 ко второмуText
:
`HStack {
Text(«A great and warm welcome to Kuchi»)
.background(Color.red)
Text(«A great and warm welcome to Kuchi»)
.layoutPriority(1)
.background(Color.red)
Text(«A great and warm welcome to Kuchi»)
.background(Color.red)
}`
Вы видите, что ему дана возможность использовать столько места, сколько нужно.

Привет, Мир 12
Теперь попробуйте добавить отрицательный приоритет к первомуText
:
`HStack {
Text(«A great and warm welcome to Kuchi»)
.layoutPriority(-1)
.background(Color.red)
Text(«A great and warm welcome to Kuchi»)
.layoutPriority(1)
.background(Color.red)
Text(«A great and warm welcome to Kuchi»)
.background(Color.red)
}`
При этом вы можете ожидать, что это будет последний элемент, который будет обработан.

Привет, Мир 13
И на самом деле ему дана очень маленькая ширина. Чтобы уравновесить это, элемент управления расширяется по вертикали.
Существует важное различие между двумя способами изменения адаптивной степени: ручная установка приоритета компоновки изменяет не только порядок сортировки, но и предлагаемый размер.
Для представлений с одинаковым приоритетом родительское представление предлагает размер, равномерно пропорциональный количеству дочерних представлений. В случае разных приоритетов родительское представление использует другой алгоритм: оно вычитает минимальный размер всех дочерних элементов с более низкими приоритетами и предлагает этот результирующий размер дочернему элементу (или дочерним элементам, если их несколько), имеющему самый высокий приоритет компоновки.
Посмотрите еще раз на результат предыдущего примера. HStack
элементы управления расположены горизонтально, поэтому ширина является наиболее ограничивающим размером, потому что дочерние представления конкурируют за ширину, тогда как по вертикали они практически не имеют ограничений.
Итак, давайте сосредоточимся на ширине:
HStack
вычисляет минимальную ширину, требуемую дочерним представлением с более низким приоритетом. Это происходитText
слева, который имеет приоритет -1 и ширина которого определяется текстом, отображаемым вертикально. Поэтому он занимает минимально возможную ширину, выделенную синим цветом на следующем увеличенном изображении:

Привет, Мир 14
HStack
находит дочернее представление с наивысшим приоритетом, которое является среднимText
, имеющим приоритет 1, самый высокий среди его дочерних элементов.

Привет, Мир 15
HStack
назначает виртуальную минимальную ширину всем дочерним представлениям, имеющим приоритет ниже максимального. Минимальная ширина-это ширина, рассчитанная на шаге 1, а количество дочерних представлений, имеющих более низкий приоритет, равно двум:Text
s слева с приоритетом -1 и справа с приоритетом 0.

Привет, Мир 16
- Учитывая ширину, имеющуюся в его распоряжении, для каждого дочернего представления с более низким приоритетом
HStack
вычитается его минимальная ширина, которая в данном случае в два раза превышает минимальную ширину, вычисленную на шаге 1. Результирующая ширина предлагается дочернему виду с наивысшим приоритетом —Text
at center.

Привет, Мир 17
- Центр
Text
at решает взять ширину, необходимую для отображения текста в одной строке.

Привет, Мир 18
В этот момент стек может обработать следующее представление Text
с приоритетом 0 с правой стороны. Алгоритм тот же; разница в том, что оставшаяся ширина теперь равна:
- Ширина в
HStack
вашем распоряжении. - Минус размер, занимаемый текстом с приоритетом 1.
- Минус минимальный размер требуемого текста с приоритетом -1.

Привет, Мир 19
Вы видите, что Text
с приоритетом 0 наилучшим образом использует имеющийся в его распоряжении размер, перенося свой текст в 4 строки. Это не оставляет никаких размеров, за которые могли бы конкурировать другие компоненты, кроме абсолютного минимума, вычисленного на шаге 1 предыдущего списка. Это гарантированный размер; это как иметь гарантированную минимальную зарплату, может быть, очень низкую, но все же гарантированную независимо от того, насколько жадны ваши начальники!
HStack и VStack
HStack
и VStack
оба являются контейнерными представлениями, и они ведут себя одинаково. Разница только в ориентации:
HStack
раскладывает подвиды горизонтальноVStack
раскладывает подвиды по вертикали
AppKit и UIKit имеют аналогичный компонентUIStackView
, который работает в двойном режиме , имея axis
свойство, которое определяет, в каком направлении расположены его подвиды.
Вы уже видели HStack
и VStack
в этой, и в предыдущих главах. Во многих случаях используется инициализатор, который принимает только представление содержимого. На самом деле он принимает два дополнительных параметра, которые поставляются со значениями по умолчанию:
`// HStack
init(
alignment: VerticalAlignment = .center,
spacing: CGFloat? = nil,
@ViewBuilder content: () -> Content
)
// VStack
init(
alignment: HorizontalAlignment = .center,
spacing: CGFloat? = nil,
@ViewBuilder content: () -> Content
)`
- выравнивание-это вертикальное и горизонтальное выравнивание соответственно для HStack и VStack, оно определяет, как выравниваются подвиды, по умолчанию
.center
в обоих случаях. - расстояние — это расстояние между детьми. Когда
nil
по умолчанию используется расстояние , зависящее от платформы. Поэтому, если вы хотите ноль, вы должны установить его явно.
content
Параметр — это обычное закрытие,которое создает дочернее представление. Но контейнеры обычно могут возвращать более одного дочернего элемента, как вы видели в примере этого раздела, где HStack
он содержит три Text
компонента.
@ViewBuilder
Атрибут-это то, что позволяет это сделать: он позволяет замыканию, которое возвращает дочернее представление, вместо этого предоставлять несколько дочерних представлений.
Примечание по выравниванию
В то время VStack
как выравнивание может иметь три возможных значения — .center
, .leading
и .trailing
— HStack
аналог немного богаче. Помимо центра, дна и верха, он также имеет два очень полезных чехла:
- firstTextBaseline: выравнивает представления на основе самого верхнего базового вида текста.
- lastTextBaseline: выравнивает представления на основе самого нижнего базового вида текста.
Они пригодятся, когда у вас есть тексты разных размеров и/или шрифтов, и вы хотите, чтобы они были выровнены визуально привлекательным образом.
Пример стоит тысячи слов, поэтому , все еще находясь внутриChallengeView
, замените его body
свойство на:
var body: some View { HStack() { Text("Welcome to Kuchi").font(.caption) Text("Welcome to Kuchi").font(.title) Button(action: {}, label: { Text("OK").font(.body) }) } }
Это выглядит как простое HStack
с двумя Text
s и a Button
, каждый из которых имеет разный размер шрифта. Если вы просмотрите его как есть, то увидите, что три дочерних элемента центрированы по вертикали:

Центр HStack
Но это выглядит не очень хорошо, не так ли? Чтобы он выглядел лучше, было бы лучше выровнять текст внизу, что вы можете сделать, указав HStack
выравнивание в его инициализаторе:
HStack(alignment: .bottom) {
Но опять же, это не очень приятно глазу:

Дно HStack
И вот тут-то на помощь могут прийти два базовых случая. Попробуйте использовать.firstTextBaseline
:
HStack(alignment: .firstTextBaseline) {
Меньший текст и кнопка теперь немного сдвинуты вверх, чтобы соответствовать базовой линии большего текста. Это выглядит намного лучше, не так ли?

Основание HStack
ZStack
Без аналога AppKit и UIKit третьим компонентом стека является ZStack
то , что дочерние представления укладываются друг на друга.
ZStack
Дочерние элементы сортируются по позиции , в которой они объявлены, что означает, что первый подвид отображается в нижней части стека, а последний-в верхней.
Интересно, .layoutPriority
что применение к дочерним представлениям не влияет на их Z-порядок, поэтому невозможно изменить порядок, в котором они определены в ZStack
теле.
Как и в случае с другими видами контейнеровZStack
, по умолчанию дочерние виды располагаются в центре контейнера.
Говоря о размере, если HStack
высота определяется самым высоким подвидом, а VStack
ширина-самым широким подвидом, то ширина и высота a ZStack
определяются соответственно самым широким и самым высоким подвидами.
ZStack
Через мгновение вы будете использовать часть представления поздравлений в приложении Kuchi.
Другие виды контейнеров
Это может показаться очевидным, но любое представление, которое может иметь дочернее представление, может стать контейнером: просто вставьте его дочерние представления в представление стека. Таким образом, компонент , такой как aButton
, который может иметь представление метки, не ограничивается одним Text
или Image
; вместо этого вы можете генерировать практически любой многовидовой контент, встраивая все в представление стека.
Представления стека также могут быть вложены друг в друга, и это очень полезно для создания сложных пользовательских интерфейсов. Однако помните, что если представление становится слишком сложным, оно может (и должно!) Быть разделено на более мелкие части.
Примечание: Представления стека ограничены только 10 дочерними элементами. Это связано с тем, что представления стека, как и другие
View
типы , инициализируются символом a@ViewBuilder
, который сам по себе может быть инициализирован до 10 представлениями. На момент написания этой статьи это легко проверить, создав стек с 11 дочерними элементами. Компилятор выдаст одно из этих загадочных сообщений об ошибках, чтобы сказать вам, что вы зашли слишком далеко.
Назад в Кучи
До сих пор эта глава состояла в основном из теории и примеров свободной формы, демонстрирующих конкретные функции или поведение. Итак, теперь пришло время испачкать руки и добиться некоторого прогресса с приложением Kuchi.
Вид поздравлений
Представление поздравления используется для поздравления пользователя после того, как он даст пять правильных ответов. Откройте CongratulationsView и взгляните на его содержимое.
`struct CongratulationsView: View {
let avatarSize: CGFloat = 120
let userName: String
init(userName: String) {
self.userName = userName
}
var body: some View {
EmptyView()
}
}`
Если вы сталкиваетесь с этим впервые EmptyView
, то это просто… пустое представление. Вы можете использовать его в качестве заполнителя везде, где ожидается представление, но у вас еще нет представления для него, либо по замыслу, либо потому, что вы его еще не создали.
Контент в этом представлении будет выложен вертикально — так что хорошим стартом будет добавление VStack
, замена пустого представления:
var body: some View { VStack { } }
Затем добавьте статическое поздравление Text
внутрь, используя большой размер шрифта серого цвета:
VStack { Text("Congratulations!") .font(.title) .foregroundColor(.gray) }

Поздравления посмотреть
Сразу после этого поздравления Text
добавьте еще одно , поменьшеText
:
Text("You’re awesome!") .fontWeight(.bold) .foregroundColor(.gray)

Поздравляю Вид 2
В нижней части этого представления должна быть кнопка для закрытия представления и возврата назад. Добавьте следующее в нижнюю часть стопки:
Button(action: { challengesViewModel.restart() }, label: { Text("Play Again") }) .padding(.top)
Метка кнопки показывает простое сообщение “Воспроизвести снова”, и действие состоит в том, чтобы сбросить статус вызова в challengesViewModel
свойстве. Но есть проблема: это свойство еще не существует в представлении. Итак, вам нужно будет добавить его.
На данный момент вы можете добавить свойство и инициализировать его inline, непосредственно внутри CongratulationsView
.
struct CongratulationsView: View { // Add this property @ObservedObject var challengesViewModel = ChallengesViewModel() ...
В следующей главе, глава 8: “Состояние и поток данных — часть I”, вы увидите, как можно сделать это свойство объектом среды, аналогично тому, как вы это делали UserManager
в предыдущей главе, глава 6: “Элементы управления и пользовательский ввод”.
Вот как выглядит представление поздравления:

Поздравляю Вид 3
Аватар пользователя
Но давайте не будем останавливаться на достигнутом — конечно, вы можете сделать это еще лучше! Как насчет добавления аватара пользователя и его имени на цветном фоне, но разделенного вертикально на две половины разного цвета?
Что-то вроде этого:

Поздравляю Вид 4

Vstack
На первый взгляд он может показаться сложным, но состоит всего из трех слоев:
- Фон, разделенный на две половины разных цветов
- Аватар пользователя
- Имя пользователя
Возможно, вы уже поняли, что вам нужно а ZStack
для его реализации.
Между двумя Text
s в VStack
, добавьте следующий код:
`// 1
ZStack {
// 2
VStack(spacing: 0) {
Rectangle()
// 3
.frame(height: 90)
.foregroundColor(
Color(red: 0.5, green: 0, blue: 0).opacity(0.2))
Rectangle()
// 3
.frame(height: 90)
.foregroundColor(
Color(red: 0.6, green: 0.1, blue: 0.1).opacity(0.4))
}
// 4
Image(systemName: «person.fill»)
.resizable()
.padding()
.frame(width: avatarSize, height: avatarSize)
.background(Color.white.opacity(0.5))
.cornerRadius(avatarSize / 2, antialiased: true)
.shadow(radius: 4)
// 5
VStack() {
Spacer()
Text(userName)
.font(.largeTitle)
.foregroundColor(.white)
.fontWeight(.bold)
.shadow(radius: 7)
}
.padding()
}
// 6
.frame(height: 180)`
Фух — это же куча кода! Но не пугайтесь — это знакомый код, который вы уже использовали в предыдущей главе. Вот что происходит:
- Вы используете a
ZStack
для наложения содержимого друг на друга - Нижний слой (тот, что добавлен первым) — это фон, который разделен на две половины.
- Каждая из двух половинок имеет фиксированную высоту 90 точек и разные цвета фона. Это говорит
VStack
о том, каким высоким он должен быть. - Это аватар пользователя, настроенный с заданным размером и полупрозрачным цветом фона, закругленными углами и некоторой тенью. Обратите внимание, как легко настроить изображение!
- Финал
VStack
содержит имя пользователя, выровненное по низу. ИспользуетсяSpacer
для того, чтобы убедиться, чтоText
он опущен на дно. ПодробнееSpacer
через минуту. - Весь этот ZStack установлен на фиксированную высоту.
Результирующий вид должен выглядеть следующим образом:

Поздравляю С Просмотром
Гораздо приятнее, правда?
Вид Спейсера
Одна вещь, о которой стоит упомянуть, — это то, как Spacer
она используется внутри VStack
на шаге 5. The VStack
contains the Spacer
и the Text
with the username — больше ничего. Поэтому вы можете задаться вопросом, зачем это вообще нужно?
Если вы удалите и Spacer
то , и VStack
другое, имя пользователя все равно будет отображаться, но оно будет центрировано по вертикали:

Поздравляю Вид 2
Чтобы нажать его вниз, вы используете a VStack
, содержащий a Spacer
вверху и Text
a внизу. Он Spacer
расширяется вдоль главной оси содержащего его стека (или в обоих направлениях, если не в стеке) — так что, как побочный эффект, он толкает Text
вниз.

Распорка
Следуя правилам компоновки, описанным в начале этой главы, вот как это работает:
- Размер
VStack
предлагается его родителем, theZStack
. VStack
обнаруживает , что дочернее представление с меньшей гибкостью компоновки являетсяText
, поэтому он предлагает размер. При отсутствии приоритета компоновки, как в данном случае, предлагаемый размер составляет половину размера, имеющегося в его распоряжении.- Он
Text
вычисляет нужный ему размер и отправляет билет обратно в системуVStack
. VStack
Вычитает размер, заявленный пользователемText
, из размера, находящегося в его распоряжении, и предлагает это сделатьSpacer
.- Тот
Spacer
, будучи гибким и неприхотливым, принимает предложение.
Задача: вид выглядел бы намного лучше, если бы кнопка была выровнена по нижней части экрана. Как ты мог это сделать?
Вероятно, существует несколько способов достижения этого результата, но это можно сделать только с Spacer
помощью s.
Для того чтобы нажать кнопку вниз, вам нужно добавить a Spacer
между кнопкой и текстом над ней:
`Text(«You’re awesome!»)
.fontWeight(.bold)
.foregroundColor(.gray)
Spacer() // <== The spacer goes here
Button(action: {
self.challengesViewModel.restart()
}, label: {
Text(«Play Again»)
})`
Однако, хотя вы и достигли желаемого результата, что-то здесь не совсем так:

Поздравляю Вид 3
Кнопка теперь закреплена внизу, но все остальное было сдвинуто вверх. Чтобы исправить это, все, что вам нужно сделать, это добавить еще Spacer
один перед первым Text
в списке.VStack
:
`VStack {
Spacer() // <== The spacer goes here
Text(«Congratulations!»)
…`
Миссия выполнена!

Поздравляю Вид 4
На данный момент вы закончили с представлением поздравлений. Он хорошо передает сообщение, теперь вы можете позаботиться о другом представлении.
Завершение представления задачи
Ранее вы использовали ChallengeView
в качестве игровой площадки для тестирования код, показанный в этой главе. Теперь вам нужно заполнить его более полезным кодом. Представление challenge предназначено для отображения вопроса и списка ответов.
Оба используют представления, определенные в QuestionView и ChoicesView. Однако представление ответов скрыто при первом отображении представления вызова и появляется, когда пользователь нажимает в любом месте экрана.
Во-первых, вам нужно добавить некоторые свойства, которые понадобятся представлению позже. Откройте ChallengeView и добавьте следующие два свойства:
`let challengeTest: ChallengeTest
@State var showAnswers = false`
Как и в предыдущих примерах, предварительный просмотр на что-то жалуется. В ChallengeView_Previews
, заменить всю previews
его реализацию, в том числе:
`// 1
static let challengeTest = ChallengeTest(
challenge: Challenge(
question: «おねがい します»,
pronunciation: «Onegai shimasu»,
answer: «Please»
),
answers: [«Thank you», «Hello», «Goodbye»]
)
static var previews: some View {
// 2
return ChallengeView(challengeTest: challengeTest)
}`
Здесь все просто:
- Вы создаете тест вызова для использования в режиме предварительного просмотра.
- Вы передаете этот тест инициализатору представления.
ChallengeView
используется внутри PracticeView
и снова ChallengeView
ожидает параметр, который вам нужно передать. Откройте PracticeViewи замените ChallengeView()
строку на:
ChallengeView(challengeTest: challengeTest!)
Принудительное разворачивание в этом случае нормально, как вы проверяете nil
в строке выше.
Теперь, когда все эти настройки устранены, вы готовы создать реальное представление задачи. Как уже упоминалось ранее, представление предназначено для отображения вопроса и списка ответов. Чтобы добиться этого, замените body
ChallengeView на:
`var body: some View {
// 1
VStack {
// 2
Button(action: {
showAnswers.toggle()
}) {
// 3
QuestionView(question: challengeTest.challenge.question)
.frame(height: 300)
}
// 4
if showAnswers {
Divider()
// 5
ChoicesView(challengeTest: challengeTest)
.frame(height: 300)
.padding()
}
}
}`
Вот что происходит:
- Два вида сложены вертикально, поэтому вы используете a
VStack
. - Эта кнопка обертывает
QuestionView
, и при нажатии она переключает видимостьChoicesView
. - Это
QuestionView
то, что, как уже упоминалось, реализовано в собственном файле. - Здесь есть некоторая условная логика, которая отображается
ChoicesView
только тогда, когдаshowAnswers
естьtrue
. - Это
ChoicesView
тоже реализовано в его собственном файле. Он получает тест вызова в качестве параметра, который вы предоставляете через свойство экземпляра.
Переделка запуска приложения
Теперь, когда представление задачи завершено, вам все еще нужно поработать над двумя другими частями приложения, чтобы запустить его:
- Измените начальный вид при запуске приложения.
- Изменить
WelcomeView
.
Первая часть очень проста, как вы уже делали в предыдущих главах. Откройте KuchiAppи замените RegisterView()
его наStarterView()
, оставив все остальное неизменным. Вот как KuchiApp
это должно выглядеть:
`@main
struct KuchiApp: App {
let userManager = UserManager()
init() {
userManager.load()
}
var body: some Scene {
WindowGroup {
StarterView()
.environmentObject(userManager)
}
}
}`
Аналогично, примените ту же замену к KuchiApp_Previews
, чтобы она выглядела так:
struct KuchiApp_Previews: PreviewProvider { static let userManager = UserManager(name: "Ray") static var previews: some View { StarterView() .environmentObject(userManager) } }
Если вы откроете StarterView, вы увидите, что он работает как прокси-представление, выбирая, какое представление отображать в зависимости от флага в диспетчере пользователей:
@ViewBuilder var body: some View { if self.userViewModel.isRegistered { WelcomeView() } else { RegisterView() } }
Если isRegistered
это правда, то в противном случае он показывает WelcomeView``RegisterView
, какой вид отображался во время запуска , прежде чем вы заменили его всего несколько минут назад.
@ViewBuilder
Атрибут, применяемый кbody
, указывает, что возвращаемое представление действительно может состоять из нескольких представлений. Хотя здесь возвращается только одно представление, оно вам нужно, потому что объявлены два представления, одно вif
ветви, а другое вelse
«s».
А теперь пришло время позаботиться о WelcomeView
себе . Вам нужно изменить его таким образом, чтобы он показывал приветственное сообщение при первом отображении, а затем переходил к практическому представлению.
Откройте WelcomeViewи добавьте эти три свойства:
@EnvironmentObject var userManager: UserManager @ObservedObject var challengesViewModel = ChallengesViewModel() @State var showPractice = false
Вы уже использовали userManager
и challengesViewModel
в других местах, здесь больше нечего сказать. showPractice
это флаг состояния, который можно использовать для определения вида отображения.
Поскольку вы ввели в представление неинициализированное свойство (userManager
), вам необходимо обновить WelcomeView_Previews
его, чтобы включить это. Во WelcomeView_Previews
— первых , сделайте это, добавив .environmentObject(UserManager())
модификатор whereWelcomeView
. Вот как это должно выглядеть:
struct WelcomeView_Previews: PreviewProvider { static var previews: some View { WelcomeView() .environmentObject(UserManager()) } }
Затем замените body
of WelcomeView
на this:
`// 1
@ViewBuilder
var body: some View {
if showPractice {
// 2
PracticeView(
challengeTest: $challengesViewModel.currentChallenge,
userName: $userManager.profile.name
)
} else {
// 3
ZStack {
WelcomeBackgroundImage()
VStack {
Text(verbatim: "Hi, \(userManager.profile.name)")
WelcomeMessageView()
// 4
Button(action: {
self.showPractice = true
}, label: {
HStack {
Image(systemName: "play")
Text(verbatim: "Start")
}
})
}
}
}
}`
Новая логика такова::
- Потому
body
что содержит пару if-else, которую вам нужно добавить@ViewBuilder
, чтобы удовлетворить компилятор. То жеStarterView
самое, что и раньше. - Если
showPractice
флаг истинен, вы показываетеPracticeView
- В противном случае перейдите на другой путь, показав приветственное сообщение
- Эта кнопка используется для подтверждения приветственного сообщения и начала практики, устанавливая
showPractice
флаг при его нажатии.
Сделав все это, вы можете запустить приложение.
Поздравляем с достижением! Вот несколько скриншотов того, как выглядит приложение.

Кучи

Кучи 2
Ленивые Стопки
Стеки очень полезны для размещения представлений в том или ином направлении. В большинстве случаев это все, что вам нужно для этой цели. Однако есть одно исключение-это когда количество просмотров, складываемых один за другим, велико.
Если вы использовали либо UITableView
или NSTableView
, то, вероятно, поняли, в чем проблема. Большое количество представлений означает много обработки для создания самих представлений и много памяти для их хранения — если пользователь никогда не прокручивает вниз или вправо до последнего элемента, это будет огромная трата циклов процессора и памяти.
Поэтому лучше загружать представления по требованию, по мере необходимости, начиная с минимума, чтобы экран не был переполнен, и продолжая загружать другие представления по мере того, как пользователь требует большего.
Это то, что делают ленивые стеки. И в отличие от своих энергичных собратьев, ленивые бывают только двух видов: горизонтальные и вертикальные, соответственноLazyHStack
, и LazyVStack
— если вы подумаете на мгновение, вы поймете, что только дурак будет складывать десятки или сотни просмотров один на другой по оси Z.
Хотя вы можете добавлять представления в стек вручную, ленивые стеки действительно сияют, когда вы перебираете источник данных, что делает ленивый стек эффективным компонентом стека, управляемого данными.
История практики
Чтобы увидеть ленивые стеки в действии, вы создадите представление истории, в котором будут отображаться все последние проблемы. Поскольку у нас еще нет никакой отслеживаемой истории, вы будете случайным образом генерировать некоторые данные.
Откройте HistoryView.swift и взгляните на его содержимое. Он определяет:
History
: Структура данных для истории, состоящая из даты и задачи.random()
иrandom(count:)
: пара методов для генерации случайной истории.HistoryView
: Представление, которое вы собираетесь реализовать сейчас. Он поставляется с парой свойств и функцией:history
: Источник исторических данных.dateFormatter
: Форматер для преобразования дат в строки.header
: представление, используемое в качестве заголовка раздела.getElement(_:)
: функция, возвращающая представление для элемента истории.
Весь этот контент-это то, с чем вы уже должны быть знакомы, поэтому нет необходимости в пошаговом руководстве, чтобы добраться туда из пустого файла.
Все, что сказано, вы можете сосредоточиться на body
собственности, которая содержит EmptyView
на данный момент. Замените это пустой ленивой вертикальной стопкой:
var body: some View { LazyVStack { } }
Теперь вам нужно перебрать все элементы history
свойства, которое на данный момент генерируется случайным образом с размером 2000 элементов. Для итерации у вас может возникнуть соблазн использовать for-in
оператор, но вы не можете — попробуйте, но все, что вы получите, — это ошибка компиляции.
Вместо этого вы будете использовать ForEach
, что выглядит как утверждение, но на самом деле это просто представление, которое может динамически генерировать контент. Его инициализатор принимает три параметра:
init( _ data: Data, id: KeyPath<Data.Element, ID>, content: @escaping (Data.Element) -> Content )
data
это коллекция для итерации.id
это ключевой путь типа элемента,History
в вашем случае указывающий на свойство, которое позволяет однозначно идентифицировать каждый элемент коллекции — такое свойство должно соответствоватьHashable
content
это представление для каждого элемента — определяется в виде замыкания, которое принимает элемент для отображения в качестве параметра.
Внутри тела LazyVStack
добавьте следующее:
ForEach(history, id: \.self) { element in }
Это перебирает все элементы history
, используя сам элемент как id
— если вы посмотрите на объявление History
, вы увидите, что он реализует Hashable
протокол.
Для отображения элемента можно использовать getElement(_:)
метод, который создает и возвращает простую ячейку:
ForEach(history, id: \.self) { element in getElement(element) }
Если вы возобновите предварительный просмотр, то увидите следующее. Совсем неплохо!

Ленивый стек 1
Если вы включите предварительный просмотр в реальном времени, вы заметите, что не можете прокручивать — содержимое исправлено. Не беспокойтесь, все, что вам нужно сделать, это встроить стек в представление прокрутки:
ScrollView { LazyVStack { ForEach(history, id: \.self) { element in getElement(element) } } }
Теперь содержимое прокручивается вертикально. Было бы неплохо добавить заголовок — и это очень легко сделать, просто встроив ForEach
его вSection
:
Section(header: header) { ForEach(history, id: \.self) { element in getElement(element) } }
Вы передаете header
свойство Section
, которое определяет текстовое представление с серым фоном.

Ленивый стек 2
Если вы запустите его через live preview, то заметите, что заголовок прокручивается вместе с остальной частью представления, но было бы лучше, если бы он оставался привязанным к вершине. Для этого вы можете использовать pinnedViews
параметр LazyVStack
инициализатора ’s, чтобы указать, что заголовки разделов должны быть закреплены.
Добавьте pinnedViews
параметр следующим образом:
LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) { Section(header: header) { ForEach(history, id: \.self) { element in getElement(element) } } }
Обратите внимание, что вы также можете определить нижний колонтитул в разделах и закрепить их.
Ключевые моменты
Еще одна длинная глава — но вы отлично справились с ней! Здесь было рассмотрено множество концепций, наиболее важными из которых являются:
- SwiftUI обрабатывает макет по-другому и легче (по крайней мере, с точки зрения разработчика), чем Auto Layout.
- Виды сами выбирают свой размер; их родители не могут навязывать размер, а только предлагают его.
- Некоторые взгляды более адаптивны, чем другие. Например,
Text
пытается приспособиться к размеру, предложенному его родителем, в то времяImage
как просто игнорирует это и отображает изображение с его собственным разрешением. - Существует три типа представлений стека:
VStack
для вертикальных макетов,HStack
для горизонтальных макетов иZStack
для укладки содержимого друг на друга. - Представления стека предлагают своим дочерним элементам размеры, начиная с наименее адаптивного и заканчивая наиболее адаптивным.
- Горизонтальные и вертикальные представления стека также имеют ленивые аналоги, которые загружают контент по требованию, в отличие от рендеринга всего заранее.
- Порядок, в котором дочерние элементы обрабатываются представлениями стека, можно изменить с помощью
layoutPriority
модификатора.
Куда идти дальше?
Чтобы узнать больше о представлениях контейнеров, обязательно посмотрите видео WWDC, которое их охватывает:
- WWDC 2019: Сессия 237 “Создание пользовательских представлений с помощью SwiftUI” apple.co/2lVpSSc
Также рекомендуется официальная документация, которой в настоящее время немного не хватает в отделе многословия, но, надеюсь, это скоро улучшится.
- Просмотр стека: Официальная документация apple.co/2lXlbr1
Есть еще несколько контейнерных представлений, которые не были рассмотрены в этой главе:
Form
Group
GroupBox
Более подробную информацию об этом вы можете найти в документации. Удачи вам в ваших приключениях с SwiftUI stack и container views!