В таких приложениях, как Maps, Find My и Stocks, есть различные нижние шторки, выдвижные панели, которые остаются видимыми все время, предоставляя пользователям мгновенный доступ к определенным функциям, не заслоняя фоновый вид и поддерживая взаимодействие.
В этой статье мы рассмотрим, как эффективно использовать presentationDetents и связанные с ними модификаторы для создания интерактивных настраиваемых представлений, как в упомянутых приложениях.
Панель и презентационные фиксаторы
Листы в SwiftUI — это способ представления модального содержимого поверх текущего представления. Традиционно листы занимают весь экран при отображении.
В Human Interface Guidelines в разделе «Платформа» для iOS и iPadOS также упоминаются листы с изменяемыми размерами.
Лист с изменяемыми размерами расширяется, когда люди прокручивают его содержимое или перетаскивают граббер — небольшой горизонтальный индикатор, который может появляться у верхнего края листа. Листы изменяют размер в соответствии с их фиксаторами, которые представляют собой определенную высоту, на которой лист органично располагается. В iPhone фиксаторы задают определенную высоту, на которой лист располагается естественным образом.
Фиксасторы были представлены на WWDC 2021 в UIKit как способ создания многоуровневого и настраиваемого опыта работы с листами, позволяющий разработчикам указывать заданную высоту листов. Презентационные фиксаторы — это предопределенные точки остановки для отображения листов в SwiftUI. Они позволяют указать несколько вариантов высоты листа, предоставляя пользователям возможность изменять размер листа в нескольких вариантах. В iOS 16 в SwiftUI появились отступы и модификаторы presentationDetents(_:), позволяющие создавать персонализированные листы без необходимости создавать представление UISheetPresentationController.
struct ContentView: View {
@State private var isSheetPresented = false
var body: some View {
Button("Show Sheet") {
isSheetPresented.toggle()
}
.sheet(isPresented: $isSheetPresented) {
Text("Create with Swift")
.presentationDetents([.medium, .fraction(0.7), .large])
}
}
}
Встроенные размеры
В SwiftUI предусмотрено два стандартных размера фиксатора:
medium: занимает примерно половину высоты экранаlarge: занимает всю высоту экрана (поведение по умолчанию)
struct ContentView: View {
@State private var isSheetPresented = false
var body: some View {
Button("Show Sheet") {
isSheetPresented.toggle()
}
.sheet(isPresented: $isSheetPresented) {
Text("Create with Swift")
.presentationDetents([.medium, .large])
}
}
}
Средний фиксатор автоматически отключается в альбомной ориентации, чтобы сохранить полезное пространство. Все листы будут использовать большие фиксаторы в альбомной ориентации независимо от того, что указано в модификаторе.
Кастомные размеры
Для более точного контроля SwiftUI предлагает несколько вариантов пользовательских фиксаторов:
fraction(_ fraction: CGFloat)— указывает процент от высоты экрана (например: .fraction(0.30) для высоты 30%)height(_ height: CGFloat)— задание точной высоты в точкахcustom(_:)— определенный вами кастомный фиксатор с рассчитанной высотой
struct ContentView: View {
@State private var isSheetPresented = false
var body: some View {
Button("Show Sheet") {
isSheetPresented.toggle()
}
.sheet(isPresented: $isSheetPresented) {
Text("Create with Swift")
.presentationDetents([.fraction(0.7), .height(100)])
}
}
}
Вы можете создать пользовательский фиксатор, следуя протоколу CustomPresentationDetent и указав его высоту.
private struct SmallDetent: CustomPresentationDetent {
static func height(in context: Context) -> CGFloat? {
return 100
}
}
private struct CompactDetent: CustomPresentationDetent {
static func height(in context: Context) -> CGFloat? {
max(50, min(context.maxDetentValue * 0.3, 200))
}
}
Первый фиксатор — это тот, который мы использовали ранее, он возвращает задержку на определенной высоте. Второй вычисляет высоту между 50 и 200 пунктами, но не более 30% от доступной высоты.
struct ContentView: View {
@State private var isSheetPresented = false
var body: some View {
Button("Show Sheet") {
isSheetPresented.toggle()
}
.sheet(isPresented: $isSheetPresented) {
Text("Create with Swift")
.presentationDetents([.custom(SmallDetent.self), .custom(CompactDetent.self)])
}
}
}
private struct SmallDetent: CustomPresentationDetent {
static func height(in context: Context) -> CGFloat? {
return 100
}
}
private struct CompactDetent: CustomPresentationDetent {
static func height(in context: Context) -> CGFloat? {
max(50, min(context.maxDetentValue * 0.3, 200))
}
}
Чтобы упростить использование в вашем приложении, расширьте PresentationDetent:
struct ContentView: View {
@State private var isSheetPresented = false
var body: some View {
Button("Show Sheet") {
isSheetPresented.toggle()
}
.sheet(isPresented: $isSheetPresented) {
Text("Create with Swift")
.presentationDetents([.small, .compact])
}
}
}
private struct SmallDetent: CustomPresentationDetent {
static func height(in context: Context) -> CGFloat? {
return 100
}
}
private struct CompactDetent: CustomPresentationDetent {
static func height(in context: Context) -> CGFloat? {
max(50, min(context.maxDetentValue * 0.3, 200))
}
}
extension PresentationDetent {
static let small = PresentationDetent.custom(SmallDetent.self)
static let compact = PresentationDetent.custom(CompactDetent.self)
}
Поведение индикатора перетаскивания
Если доступен только один фиксатор, лист не будет отображать индикатор перетаскивания.
Видимость индикатора перетаскивания регулируется автоматически:
- Один фиксатор: индикатор перетаскивания скрыт
- Несколько фиксаторов: индикатор перетаскивания виден
Вы можете переопределить это поведение с помощью модификатора presentationDragIndicator(_:). Установите видимость на visible, чтобы индикатор отображался, даже если определен только один фиксатор.
struct ContentView: View {
@State private var isSheetPresented = false
var body: some View {
Button("Show Sheet") {
isSheetPresented.toggle()
}
.sheet(isPresented: $isSheetPresented) {
Text("Create with Swift")
.presentationDetents([.medium])
.presentationDragIndicator(.visible)
}
}
}
Или вы можете установить видимость на hidden, чтобы скрыть индикатор даже при наличии нескольких фиксаторов:
struct ContentView: View {
@State private var isSheetPresented = false
var body: some View {
Button("Show Sheet") {
isSheetPresented.toggle()
}
.sheet(isPresented: $isSheetPresented) {
Text("Create with Swift")
.presentationDetents([.medium, .large])
.presentationDragIndicator(.hidden)
}
}
}
Динамическая проверка или управление высотой
Вы можете захотеть изменить содержимое боттом шит в зависимости от доступного пространства. Для этого можно динамически проверять или контролировать высоту листа с помощью биндинга:
struct ContentView: View {
@State private var selectedDetent: PresentationDetent = .medium
@State private var isSheetPresented = false
var body: some View {
Button("Show Sheet") {
isSheetPresented.toggle()
}
.sheet(isPresented: $isSheetPresented) {
sheetContent
.presentationDetents([.height(100), .medium, .fraction(0.7), .large], selection: $selectedDetent)
}
}
@ViewBuilder
private var sheetContent: some View {
switch selectedDetent {
case .medium:
Text("Medium Detent")
case .fraction(0.7):
Text("Fraction Detent")
case .large:
Text("Large Detent")
case .height(100):
Text("Height Detent")
default:
Text("Unknown Detent")
}
}
}
Предотвращение удаления листа
В приведенных выше примерах пользователь может удалить лист. Чтобы воспроизвести интерфейсы типа Maps, вы можете предотвратить интерактивное удаление листа пользователем с помощью модификатора interactiveDismissDisabled(_:) и заставить его всегда быть представленным, установив постоянное значение для параметра isPresented с помощью метода Binding.constant(_:).
struct ContentView: View {
@State private var selectedDetent: PresentationDetent = .height(100)
var body: some View {
Text("Create with Swift")
.sheet(isPresented: .constant(true)) {
VStack {
Image(systemName: "swift")
.resizable()
.scaledToFit()
.padding()
.foregroundStyle(Color.accentColor)
if (selectedDetent != .height(100)) {
Text("We can’t wait to see what you will Create with Swift!")
}
}
.presentationDetents([.height(100), .medium, .large], selection: $selectedDetent)
.interactiveDismissDisabled()
}
}
}
Лист все еще можно отключить программно или указать, при каком условии лист может быть свернут, с помощью параметра isDisabled.
Взаимодействие с фоном
Во всех предыдущих реализациях листа фоновое представление оставалось неинтерактивным. В конфигурациях по умолчанию фоновое представление обычно неинтерактивно, пока боттом шит активен, что предотвращает случайные касания. Если вы хотите позволить пользователю взаимодействовать с фоновым представлением во время отображения листа, используйте модификатор presentationBackgroundInteraction(_:).
struct ContentView: View {
@State private var selectedDetent: PresentationDetent = .height(100)
@State private var selectedColor: Color = Color.accentColor
var body: some View {
VStack {
Button("Change Color", action: {
selectedColor = selectedColor == .accentColor ? .white : .accentColor
})
.padding(.top)
Spacer()
}
.sheet(isPresented: .constant(true)) {
VStack {
Image(systemName: "swift")
.resizable()
.scaledToFit()
.padding()
.foregroundStyle(selectedColor)
if (selectedDetent != .height(100)) {
Text("We can’t wait to see what you will Create with Swift!")
}
}
.presentationDetents([.height(100), .medium, .large], selection: $selectedDetent)
.interactiveDismissDisabled()
.presentationBackgroundInteraction(.enabled)
}
}
}
Условное взаимодействие с фоном
С помощью параметра upThrough можно указать, на каком зафиксированном уровне разрешено взаимодействие с фоном. Например, .enabled(upThrough: .medium) позволяет взаимодействовать с фоновым представлением только тогда, когда лист зафиксирован на среднем уровне или ниже, в то время как на большем уровне фон остается неинтерактивным.
struct ContentView: View {
@State private var selectedDetent: PresentationDetent = .height(100)
@State private var selectedColor: Color = Color.accentColor
var body: some View {
VStack {
Button("Change Color", action: {
selectedColor = selectedColor == .accentColor ? .white : .accentColor
})
.padding(.top)
Spacer()
}
.sheet(isPresented: .constant(true)) {
VStack {
Image(systemName: "swift")
.resizable()
.scaledToFit()
.padding()
.foregroundStyle(selectedColor)
if (selectedDetent != .height(100)) {
Text("We can’t wait to see what you will Create with Swift!")
}
}
.presentationDetents([.height(100), .medium, .large], selection: $selectedDetent)
.interactiveDismissDisabled()
.presentationBackgroundInteraction(.enabled(upThrough: .height(100)))
}
}
}
Мы рассмотрели, как с помощью модификаторов фиксации и фонового взаимодействия в SwiftUI можно легко реализовать сложные интерфейсы. Благодаря сочетанию настраиваемых фиксаторов, индикаторов перетаскивания и взаимодействия с фоном разработчики теперь могут создавать листы, которые улучшают, а не прерывают пользовательский опыт. Эти возможности позволяют создавать целостные интерактивные интерфейсы, которые сохраняют контекст, предоставляя при этом дополнительную функциональность.

