Тема данной статьи  —  представление нижнего всплывающего экрана (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
}

}
  1. Определяем modalPresentationStyle как .pageSheet, таким образом сообщая системе о намерении использовать данный контроллер представления в виде экрана. 
  2. Устанавливая для isModalInPresentation значение false, позволяем пользователю интерактивно закрывать экран, что не представляется возможным при установке значения true
  3. Для простоты демонстрируем только лишь оранжевое представление. 

Отображаем нижний всплывающий экран в 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)
}

...
}
  1. Прежде всего инициализируем BottomSheetViewController.
  2. Получаем встроенное свойство sheetPresentationController. Оно устанавливается для контроллера представления, когда modalPresentationStyle является .pageSheet или .formSheet. Ранее свойству BottomSheetViewController был задан стиль .pageSheet, что подтверждает наличие sheetPresentationController
  3. Свойство detents предоставляет конфигурации размеров для нижнего всплывающего экрана. В настоящее время Apple располагает только .medium() и .large(). Поскольку API для создания фиксаторов размеров (detents) разработчикам недоступно, то нет возможности установить пользовательское значение для высоты экрана. Возможно, со временем в API произойдут изменения и в нем появятся такие фиксаторы, как .small() или .custom(size: CGSize)
  4. Свойство largestUndimmedDetentIdentifier определяет, когда представление за нижним всплывающим экраном должно затемняться. Устанавливая для него значение .medium, мы инструктируем систему затемнить фон, только когда экран принимает размер .large()
  5. Свойство 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)
}
  1. Через две секунды после представления экрана запускаем данный блок кода. 
  2. С помощью замыкания animateChanges инструктируем систему выполнить анимацию изменений в свойствах экрана. В данном примере кода дается указание изменить фиксатор высоты на large и представить все в виде анимации. 

Подведем итоги и перечислим несколько моментов, которые необходимо учитывать, принимая решения о реализации нижнего всплывающего экрана своими силами или при помощи UISheetPresentationController

  • API UISheetPresentationController поддерживает только iOS 15 и более новые версии. 
  • В настоящее время возможности API ограничены, поскольку он предоставляет только конфигурации размеров .medium() и .large()
  • Допустим, нужно устранить такой параллакс-эффект: 

Для этого потребуется действовать вопреки инструкциям API, а именно предоставить detents в порядке убывания: sheet.detents = [.large(), .medium()].

Этот шаг приведет к нужному результату: 

Однако данное решение отдает “запашком”, поскольку в момент представления экрана он сразу же отображается в размере .large(). Более того, в документации Apple настоятельно рекомендуется использовать порядок возрастания

Дополнительные ресурсы 

Данный проект доступен на GitHub. Надеемся, материал был для вас полезен. Благодарим за внимание!

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Zafar Ivaev: How to Present Customizable Bottom Sheets in iOS 15

Предыдущая статьяБлокчейн и искусственный интеллект - мощный тандем
Следующая статьяПринципы SOLID - ключи к чистому коду