Источник: Nuances of Programming
Тема данной статьи — представление нижнего всплывающего экрана (bottom sheet) с помощью API UISheetPresentationController, предусмотренного в iOS 15. С исходным кодом проекта можно ознакомиться в завершающем разделе руководства. Изучив материал вы узнаете:
- как представить любой UIViewController в виде нижнего всплывающего экрана;
- как задать его размеры;
- как настроить макет и поведение такого экрана;
- что следует учитывать, принимая решение об использовании UISheetPresentationController.
Ниже представлен ожидаемый результат:
Начальный этап
Начнем с UIViewController, который отображает кнопку в центре основного экрана:
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var button: UIButton!
@IBAction func buttonHandler(_ sender: UIButton) {
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
Внутри buttonHandler реализуем нижний всплывающий компонент и отобразим его на основном экране.
Но прежде создадим другой UIViewController, который будет функционировать как требуемый компонент:
import UIKit
class BottomSheetViewController: UIViewController {
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nil, bundle: nil)
// 1
self.modalPresentationStyle = .pageSheet
// 2
self.isModalInPresentation = false
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
// 3
self.view.backgroundColor = .systemOrange
}
}
- Определяем modalPresentationStyle как .pageSheet, таким образом сообщая системе о намерении использовать данный контроллер представления в виде экрана.
- Устанавливая для isModalInPresentation значение false, позволяем пользователю интерактивно закрывать экран, что не представляется возможным при установке значения true.
- Для простоты демонстрируем только лишь оранжевое представление.
Отображаем нижний всплывающий экран в buttonHandler:
import UIKit
class ViewController: UIViewController {
...
@IBAction func buttonHandler(_ sender: UIButton) {
// 1
let vc = BottomSheetViewController()
// 2
if let sheet = vc.sheetPresentationController {
// 3
sheet.detents = [.medium(), .large()]
// 4
sheet.largestUndimmedDetentIdentifier = .medium
// 5
sheet.prefersScrollingExpandsWhenScrolledToEdge = true
// 6
sheet.prefersGrabberVisible = true
}
// 7
self.present(vc, animated: true, completion: nil)
}
...
}
- Прежде всего инициализируем BottomSheetViewController.
- Получаем встроенное свойство sheetPresentationController. Оно устанавливается для контроллера представления, когда modalPresentationStyle является .pageSheet или .formSheet. Ранее свойству BottomSheetViewController был задан стиль .pageSheet, что подтверждает наличие sheetPresentationController.
- Свойство detents предоставляет конфигурации размеров для нижнего всплывающего экрана. В настоящее время Apple располагает только .medium() и .large(). Поскольку API для создания фиксаторов размеров (detents) разработчикам недоступно, то нет возможности установить пользовательское значение для высоты экрана. Возможно, со временем в API произойдут изменения и в нем появятся такие фиксаторы, как .small() или .custom(size: CGSize).
- Свойство largestUndimmedDetentIdentifier определяет, когда представление за нижним всплывающим экраном должно затемняться. Устанавливая для него значение .medium, мы инструктируем систему затемнить фон, только когда экран принимает размер .large().
- Свойство prefersScrollingExpandsWhenScrolledToEdge обслуживает те ситуации, когда в нижнем всплывающем экране имеется UIScrollView. Если данное свойство true, то при максимальном развертывании экрана пользователь может просматривать его содержимое. В противном случае мы утрачиваем возможность прокручивать UIScrollView внутри нижнего всплывающего экрана. Ниже представлен пример работы при значении true:
А вот, что происходит в случае с false:
6. prefersGrabberVisible устанавливается в значение true для показа элемента захвата в верхней части экрана:
7. На завершающей стадии отображаем нижний всплывающий экран с помощью стандартного метода present().
Помимо ранее рассмотренных также применяется свойство радиуса углов preferredCornerRadius. При его равенстве 0 получаем следующий результат:
Изменение размера программным способом
Можно изменить текущее значение Detent и осуществить анимацию этих изменений программным способом:
@IBAction func buttonHandler(_ sender: UIButton) {
let vc = BottomSheetViewController()
if let sheet = vc.sheetPresentationController {
...
// 1
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
// 2
sheet.animateChanges {
sheet.selectedDetentIdentifier = .large
}
}
}
self.present(vc, animated: true, completion: nil)
}
- Через две секунды после представления экрана запускаем данный блок кода.
- С помощью замыкания animateChanges инструктируем систему выполнить анимацию изменений в свойствах экрана. В данном примере кода дается указание изменить фиксатор высоты на large и представить все в виде анимации.
Подведем итоги и перечислим несколько моментов, которые необходимо учитывать, принимая решения о реализации нижнего всплывающего экрана своими силами или при помощи UISheetPresentationController:
- API UISheetPresentationController поддерживает только iOS 15 и более новые версии.
- В настоящее время возможности API ограничены, поскольку он предоставляет только конфигурации размеров .medium() и .large().
- Допустим, нужно устранить такой параллакс-эффект:
Для этого потребуется действовать вопреки инструкциям API, а именно предоставить detents в порядке убывания: sheet.detents = [.large(), .medium()].
Этот шаг приведет к нужному результату:
Однако данное решение отдает “запашком”, поскольку в момент представления экрана он сразу же отображается в размере .large(). Более того, в документации Apple настоятельно рекомендуется использовать порядок возрастания:
Дополнительные ресурсы
Данный проект доступен на GitHub. Надеемся, материал был для вас полезен. Благодарим за внимание!
Читайте также:
Перевод статьи Zafar Ivaev: How to Present Customizable Bottom Sheets in iOS 15