Site icon AppTractor

Исследуем интерактивный Bottom Sheet в SwiftUI

В таких приложениях, как 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 предусмотрено два стандартных размера фиксатора:

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 предлагает несколько вариантов пользовательских фиксаторов:

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)
}

Фиксаторы автоматически упорядочиваются снизу вверх. В примере выше вы можете заметить, что независимо от порядка первый фиксатор имеет высоту 100.

Поведение индикатора перетаскивания

Если доступен только один фиксатор, лист не будет отображать индикатор перетаскивания.

Видимость индикатора перетаскивания регулируется автоматически:

Вы можете переопределить это поведение с помощью модификатора 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 можно легко реализовать сложные интерфейсы. Благодаря сочетанию настраиваемых фиксаторов, индикаторов перетаскивания и взаимодействия с фоном разработчики теперь могут создавать листы, которые улучшают, а не прерывают пользовательский опыт. Эти возможности позволяют создавать целостные интерактивные интерфейсы, которые сохраняют контекст, предоставляя при этом дополнительную функциональность.

Источник

Exit mobile version