Обработка свайпа при закрытии модального окна. UIAdaptivePresentetionDelegate.

 

Обработка свайпа при закрытии модального окна. UIAdaptivePresentetionDelegate

До выхода iOS 13 модальные окна открывались в полноэкранном режиме. Выглядело это так:

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

В соответсвии с новым iOS SDK по умолчанию модальные окна открываются в режиме Page Sheet. Это нововведение изменило не только внешний вид, но и принцип взаимодействия с окном. Сейчас окна открываются стопкой и для возврата на предыдущий экран достаточно свайпнуть окно вниз. Если свайп ни как не обрабатывать, то это действие просто закроет окно без сохранения внесенных изменений. По сути данный жест дублирует кнопку Cancel, по нажатию на которую происходит вызов метода dismiss. Но в отличии от кнопки Cancel, которая явно определяет последующее действие (закрытие окна без сохранения), желание пользователя при закрытии окна свайпом не всегда очевидно. Хочет ли он просто закрыть окно, или же ожидает, что внесенные им данные будут сохранены? А может быть пользователь вообще свайпнул случайно и на самом деле он не хотел делать этого? В общем мы, как разработчики не должны гадать, чего на самом деле хочет пользователь, когда закрывает окно свайпом. Самым правильным решением в таком случае будет спросить пользователя о том, что действительно он хочет сделать: сохранить внесенные данные и закрыть окно? Выйти без сохранения? Или может быть вообще остаться и продолжить редактирование? Что бы выяснить намерения пользователя мы можем контролировать свайп экрана вниз и в зависимости от ситуации предлагать пользователю то или иное решение. Для этого надо подписаться под протокол UIAdaptivePresentetionDelegate:

extension NewContactViewController: UIAdaptivePresentationControllerDelegate {
    
}

Данный протокол содержит методы, которые определяют как реагировать на закрытие окна свайпом. Например метод presentationControllerDidDismiss вызывается если окно было закрыто:

func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
    print(#function)
}

При закрытии окна свайпом мы получим на консоль уведомление с названием метода.

Есть еще метод presentationControllerDidAttemptToDismiss. Но он не будет вызван, если мы просто закроем окно свайпом вниз. Вызов данного метода происходит только в том случае, когда свойство isModalInPresentation класса UIViewController имеет значение true. Данное свойство указывает, применяет ли вью контроллер модальное поведение или нет. Если да, то закрыть окно свайпом вниз не получится:

override func viewDidLoad() {
    super.viewDidLoad()
    
    isModalInPresentation = true
}
extension NewContactViewController: UIAdaptivePresentationControllerDelegate {
    
    func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
        print(#function)
    }
    
    func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
        print(#function)
    }
}

В итоге при попытке свайпнуть окно вниз будет вызван метод presentationControllerDidAttemptToDismiss, при этом закрыть окно свайпом уже не получится:

Какие из этого можно сделать выводы? Мы можем следить за текстовыми полями и в том случае, если пользователь их заполнил, то присваивать свойству isModalInPresentation значение true. В таком случае закрытие окна свайпом будет заблокировано. Кроме того при попытке свайпнуть окно будет вызван метод presentationControllerDidAttemptToDismiss из которого в свою очередь мы можем вызвать меню пользовательских действий:

// Метод в котором отслеживается ввод данных в текстовое поле
@objc private func firstNameTextFieldDidChanged() {
    guard let firstName = firstNameTextField.text else { return }
    doneButton.isEnabled = !firstName.isEmpty ? true : false
    isModalInPresentation = !firstName.isEmpty ? true : false
}

В данном методе мы следим за вводом данных в текстовое поле. Если поле не пустое, то включаем кнопку Done (по умолчанию она отключена) и блокируем закрытие окна свайпом вниз.

Как и было сказано выше при попытке свайпнуть экран с заполненными полями будет вызван метод presentationControllerDidAttemptToDismiss который в свою очередь вызовет меню пользовательских действий:

extension NewContactViewController: UIAdaptivePresentationControllerDelegate {
    func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
        showAlertSheet()
    }
}

В итоге на выходе мы получим такой результат:

Т.е. если текстовые поля не заполнены, то свайп позволяет закрывать окно без каких либо предупреждений. Если же пользователь внес какие то данные, то при свайпе окна ему предоставляется выбрать определенное действие.