В iOS 13 модальные презентации, использующие стиль формы и листа страницы, можно закрыть с помощью жеста панорамирования вниз. Это проблематично в одном из моих листов формы, потому что пользователь рисует в этом поле, что мешает жесту. Он тянет экран вниз вместо того, чтобы рисовать вертикальную линию.
Как вы можете отключить вертикальный жест, чтобы закрыть жест в контроллере модального представления, представленном в виде листа?
Настройка isModalInPresentation = true по-прежнему позволяет тянуть лист вниз, просто он не закрывается.
Но они не объясняют, как решить, когда этот жест мешает другим, как задается этот вопрос.





Этот жест можно найти в свойстве presentedView контроллера модального представления. Как я отлаживал, массив gestureRecognizers этого свойства имеет только один элемент, и его печать привела к чему-то вроде этого:
UIPanGestureRecognizer: 0x7fd3b8401aa0 (_UISheetInteractionBackgroundDismissRecognizer);
Итак, чтобы отключить этот жест, вы можете сделать следующее:
let vc = UIViewController()
self.present(vc, animated: true, completion: {
vc.presentationController?.presentedView?.gestureRecognizers?[0].isEnabled = false
})
Чтобы снова включить его, просто установите isEnabled обратно на true:
vc.presentationController?.presentedView?.gestureRecognizers?[0].isEnabled = true
Обратите внимание, что iOS 13 все еще находится в стадии бета-тестирования, поэтому в следующем выпуске может быть добавлен более простой подход.
Хотя это решение работает на данный момент, я бы не рекомендовал его, так как оно может не работать в некоторых ситуациях или может быть изменено в будущих выпусках iOS и, возможно, повлияет на ваше приложение.
Обратите внимание, что если у вас есть представление прокрутки в этом контроллере представления, кажется, что вы все еще можете потянуть вниз, чтобы закрыть, как только вы достигли вершины. Но если у вас нет прокрутки, это здорово.
@JordanH Верно! Кажется, другой жест из представления прокрутки также каким-то образом обрабатывает модальное закрытие. Я напечатал жесты просмотра прокрутки, и там есть UISwipeDismissalGestureRecognizer. Это может быть проблемой.
UITableView также создает _UISwipeDismissalGestureRecognizer. Более того, если вы создадите навигационный контроллер с корневым контроллером представления, модально представите стек в виде листа страницы/формы и поместите другой контроллер представления поверх него, жест прокрутки вниз для закрытия закроет весь стек через распознаватель жестов, созданный где-то выше по иерархии UIView. Без явной поддержки Apple для отключения обработки сенсорных событий смахиванием вниз, чтобы закрыть, единственным надежным решением (начиная с Xcode 11 beta 3) является использование UIModalPresentationStyle из UIModalPresentationFullScreen.
Вы даже можете выполнять поиск по имени или типу, если хотите быть в большей безопасности в отношении того, какой жест вы отключаете. for gesture in guestures where gesture.name == "_UISheetInteractionBackgroundDismissRecognizer" { gesture.isEnabled = false }
Для контроллера навигации, чтобы избежать взаимодействия с прокруткой для представленного представления, мы можем использовать:
if #available(iOS 13.0, *) {navController.isModalInPresentation = true}
Нет, но это не мешает жест, о чем и идет речь.
Как правило, не следует пытаться отключить функцию смахивания для закрытия, так как пользователи ожидают, что все листы форм/страниц будут вести себя одинаково во всех приложениях. Вместо этого вы можете рассмотреть возможность использования стиля полноэкранного представления. Если вы хотите использовать лист, который нельзя закрыть с помощью смахивания, установите isModalInPresentation = true, но обратите внимание, что это по-прежнему позволяет листу опускаться вертикально, и он подпрыгивает обратно после отпускания касания. Ознакомьтесь с документацией по UIAdaptivePresentationControllerDelegate, чтобы реагировать, когда пользователь пытается закрыть его, проведя пальцем по экрану, помимо других действий.
Если у вас есть сценарий, в котором жесты вашего приложения или обработка касаний зависят от функции прокрутки для закрытия, я получил несколько советов от инженера Apple о том, как это исправить.
Если вы можете предотвратить запуск распознавателя жестов панорамирования системы, это предотвратит отклонение жестов. Несколько способов сделать это:
Если ваш рисунок на холсте выполнен с помощью распознавателя жестов, такого как ваш собственный подкласс UIGestureRecognizer, войдите в фазу began до того, как это сделает жест закрытия листа. Если вы распознаете так же быстро, как UIPanGestureRecognizer, вы выиграете, и жест закрытия листа будет подорван.
Если ваш рисунок на холсте выполняется с помощью распознавателя жестов, настройте требование динамического отказа с помощью -shouldBeRequiredToFailByGestureRecognizer: (или связанного с ним метода делегата), где вы возвращаете NO, если переданный распознаватель жестов является UIPanGestureRecognizer.
Если ваш рисунок на холсте выполняется с ручной обработкой касаний (например, touchesBegan:), переопределите -gestureRecognizerShouldBegin в представлении обработки касаний и верните NO, если переданный распознаватель жестов является UIPanGestureRecognizer.
С моей установкой №3 оказалось, что она работает очень хорошо. Это позволяет пользователю провести пальцем вниз в любом месте за пределами холста для рисования, чтобы закрыть (например, панель навигации), и в то же время позволяет пользователю рисовать, не перемещая лист, как и следовало ожидать.
Я не могу рекомендовать пытаться найти жест, чтобы отключить его, так как он кажется довольно динамичным и может повторно включаться, например, при переключении между классами разных размеров, и это может измениться в будущих выпусках.
Я последовал #2, но в итоге использовал метод делегата gestureRecognizer(_:,shouldRecognizeSimultaneouslyWith:), чтобы позволить некоторым распознавателям работать вместе, а другим нет.
Кажется, я не могу заставить что-либо из этого работать? Любые советы для моего сценария? Я представляю экран с системой выбора фотографий, используя перетаскивание, чтобы выбрать то, что использует стандартное приложение для фотографий iOS. Это использует распознаватель жестов панорамирования и UICollectionView. Простите меня, если я ошибаюсь, но не должны ли мы стать делегатом отклоняющего распознавателя, чтобы использовать любой из этих методов?
Не бери в голову! Использовал shouldRequireFailureOf вместо shouldBeRequiredToFailBy ?♂️
Приходя из Android, я искал способ, которым мои пользовательские представления могли бы взять на себя сенсорное управление, и жестRecognizerShouldBegin идеален! Спасибо!
№ 3 отлично сработал для моего приложения, которое позволяет пользователю рисовать в представлении.
@Jordan H Не могли бы вы лучше объяснить № 3 с помощью кода?
@ jordan-h У меня тоже проблемы с работой #3. Я добавил разные распознаватели (касание, панорамирование, длительное нажатие...) как в вид контроллера представления, так и в вид чертежа, но безуспешно. Модальное окно все еще перемещается при рисовании на виде.
Отличная работа! Очень интересно. №3 работает идеально для меня.
Вся эта дополнительная работа, потому что Apple захотела изменить стиль модального представления...
Для # 2 мне нужно было отличить UIPanGestureRecognizer, которым я владел, от одного, контролирующего UITableView/UIScrollView, и другого, контролирующего модальное окно. Все три из них являются UIPanGestureRecognizers, поэтому проверка их типа не помогает. Следующее, что лучше всего сделать, это проверить представление, на которое они нацелены. Модальный распознаватель жестов нацелен на UIDropShadowView, который является частным API, поэтому вы не можете проверить это явно. Вместо этого, в зависимости от того, как устроена иерархия ваших представлений, вы можете проверить, не является ли это UIDropShadowView вашим собственным подпредставлением, чтобы исключить его.
# 3 очень хорошо работает для меня с формой захвата подписи
# 3 также работал для UIControl, который реализовал методы «отслеживания» для обработки касаний — я использовал это внутри func: `return isTracking? !(gestureRecognizer — это UIPanGestureRecognizer): true`
Используйте это в представленном ViewController viewDidLoad:
if #available(iOS 13.0, *) {
self.isModalInPresentation = true
}
Как отмечалось в вопросе, isModalInPresentation = true по-прежнему позволяет вытягивать лист, он просто не убирается, что может быть именно тем, что вам нужно, или может быть проблематичным в зависимости от вашего варианта использования, как это было для моего холста для рисования.
Ты прав. Я думаю, что самое простое решение — использовать старый полноэкранный модальный стиль: self.modalPresentationStyle = .fullScreen
viewController.isModalInPresentation = true сработало для меня
это правильный ответ, вы все еще сохраняете анимацию опускания, но вы никогда не закрываете вид этим. Спасибо!
Работает на меня. Это лучше, чем удаление распознавателей жестов.
вы можете изменить стиль презентации, если она в полноэкранном режиме, раскрывающееся меню для закрытия будет отключено.
navigationCont.modalPresentationStyle = .fullScreen
Я думаю, что это правильный ответ. На самом деле я использовал это вместе с isModalInPresentation, просто чтобы быть уверенным, и это работает отлично. Ключом для меня было установить их в родителях. Когда я попытался установить viewDidLoad в представленном контроллере, это НЕ сработало.
для меня я думаю, что это также правильный способ справиться с этим. Если это полноэкранный режим, вы не можете смахнуть и закрыть его? пробовал, не получается, так что это правильно
Вы можете использовать метод презентацииControllerDidAttemptToDismiss UIAdaptivePresentationControllerDelegate и отключить распознавание жестов в представленном представлении. Что-то вроде этого:
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
presentationController.presentedView?.gestureRecognizers?.first?.isEnabled = false
}
Я, я использую это:
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
for(UIGestureRecognizer *gr in self.presentationController.presentedView.gestureRecognizers) {
if (@available(iOS 11.0, *)) {
if ([gr.name isEqualToString:@"_UISheetInteractionBackgroundDismissRecognizer"]) {
gr.enabled = false;
}
}
}
Для каждого тела, имеющего проблемы с решением №3 Джордана.
Вам нужно искать ROOT-контроллер представления, который представлен, в зависимости от вашего стека представлений, возможно, это не ваше текущее представление.
Пришлось искать свои навигационные контроллеры PresentationViewController.
Кстати @Jordam: Спасибо!
UIGestureRecognizer *gesture = [[self.navigationController.presentationController.presentedView gestureRecognizers] firstObject];
if ([gesture isKindOfClass:[UIPanGestureRecognizer class]]) {
UIPanGestureRecognizer * pan = (UIPanGestureRecognizer *)gesture;
pan.delegate = self;
}
В моем случае у меня есть модальный экран с представлением, которое получает касания для захвата подписей клиентов.
Отключение распознавателя жестов в навигационном контроллере решило проблему, предотвратив запуск модального интерактивного закрытия вообще.
Следующие методы реализованы в нашем контроллере модального представления и вызываются через делегат из нашего пользовательского представления подписи.
Звонили из touchesBegan:
private func disableDismissalRecognizers() {
navigationController?.presentationController?.presentedView?.gestureRecognizers?.forEach {
$0.isEnabled = false
}
}
Звонили из touchesEnded:
private func enableDismissalRecognizers() {
navigationController?.presentationController?.presentedView?.gestureRecognizers?.forEach {
$0.isEnabled = true
}
}
Вот GIF, показывающий поведение:

Этот вопрос, помеченный как дубликат, лучше описывает мою проблему: Отключение интерактивного закрытия представленного контроллера представления в iOS 13 при перетаскивании из основного представления
Это не работает на iOS 14; navigationController?.presentationController?.presentedView всегда nil. Я пытался вместо этого использовать navigationController?.presentationController?.presentedViewController.view, но не могу найти _UISheetInteractionBackgroundDismissRecognizer там...
Странно, только что протестировали на iOS 14, наше приложение отлично работает. Построен с Xcode 12, не уверен, что это будет иметь значение
Моя ошибка, я снова открыл какой-то старый проект и забыл, что это означало исправить что-то на другом экране, он отлично работает на iOS 14, спасибо за подтверждение и спасибо за ваш ответ!
В моем случае navigationController всегда равен нулю, поэтому я смог получить к нему доступ с помощью presentationController?.presentedView?.gestureRecognizers?.forEach
У меня почти сработало, за исключением того, что распознаватели жестов, которые нужно было заблокировать, были self.presentationController?.presentedView?.gestureRecognizers?, а не navigationController?.presentationController?.presentedView?.gestureRecognizers?. См. ответ @M Reza
Постараюсь более подробно описать метод 2, уже предложенный @Jordan H:
1) Чтобы иметь возможность улавливать и принимать решения о жесте панорамирования модального листа, добавьте это в контроллер представления viewDidLoad:
navigationController?.presentationController?.presentedView?.gestureRecognizers?.forEach {
$0.delegate = self
}
2) Включите возможность ловить жест панорамирования вместе с вашими собственными жестами с помощью gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:)
3) Фактическое решение может быть отправлено gestureRecognizer(_:shouldBeRequiredToFailBy:)
Пример кода, который делает жест смахивания более предпочтительным, чем жест панорамирования листа, если оба присутствуют. Это не влияет на исходный жест панорамирования в областях, где нет распознавателя жестов смахивания, и поэтому исходный «смахивание для закрытия» может по-прежнему работать так, как задумано.
extension PeopleViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer === UIPanGestureRecognizer.self && otherGestureRecognizer === UISwipeGestureRecognizer.self {
return true
}
return false
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
В моем случае у меня есть только несколько распознавателей жестов смахивания, поэтому мне достаточно сравнения типов, но если их больше, может иметь смысл сравнить сами распознаватели жестов (либо добавленные программно, либо как выходы из построителя интерфейса), как описано в этот документ: https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/coordinating_multiple_gesture_recognizers/preferring_one_gesture_over_another
Вот как код работает в моем случае. Без него жест смахивания в основном игнорировался и срабатывал лишь изредка.
Эй, я попробовал ваше решение, но у меня все еще есть проблема. Когда я устанавливаю делегата в viewDidAppear (потому что мой presentationController равен нулю, я просто морально представляю vc). И я повторяю супервизор его представления, чтобы узнать, что представление имеет PanGesture, и устанавливаю его делегата на self. Тогда мой vc не может смахнуть вниз, чтобы закрыть, есть ли другой способ решить мою проблему? Пожалуйста помоги
@Weslie, вам нужно поймать распознаватели представленного представления, а не распознаватели его суперпредставления. Попробуйте временно добавить метод делегатаgestRecognizerShouldBegin, чтобы увидеть, какие жесты на самом деле перехватываются и отлаживаются.
в iOS 13
if #available(iOS 13.0, *) {
obj.isModalInPresentation = true
} else {
// Fallback on earlier versions
}
В случае, когда UITableView или UICollectionView инициирует жест закрытия листа страницы, когда пользователь пытается прокрутить за верхний конец прокручиваемого представления, этот жест можно отключить, добавив невидимый UIRefreshControl, который немедленно вызывает endRefreshing.
При подготовке (для: отправителя :) :
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == viewControllerSegueID {
let controller = segue.destination as! YourViewController
controller.modalPresentationStyle = .fullScreen
}
}
или после инициализации контроллера:
let controller = YourViewController()
controller.modalPresentationStyle = .fullScreen
Сначала вы можете получить ссылку на UIPanGestureRecognizer, обрабатывающий отклонение листа страницы в методе viewDidAppear(). Обратите внимание, что эта ссылка равна нулю в viewWillAppear() или viewDidLoad(). Затем вы просто отключите его.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
presentationController?.presentedView?.gestureRecognizers?.first.isEnabled = false
}
Если вам нужны дополнительные настройки, а не полное отключение, например, при использовании navBar на листе страницы, установите делегат этого UIPanGestureRecognizer на свой собственный контроллер представления. Таким образом, вы можете отключить распознаватель жестов исключительно в своем contentView, оставив его активным в области navBar, внедрив
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {}
Не нужно изобретать велосипед. Это так же просто, как принять протокол UIAdaptivePresentationControllerDelegate на вашем пункт назначенияViewController, а затем применить соответствующий метод:
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
return false
}
Например, предположим, что ваш destinationViewController подготовлен к переходу, как показано ниже:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "yourIdentifier",
let destinationVC = segue.destination as? DetailViewController
{
//do other stuff
destinationVC.presentationController?.delegate = destinationVC
}
}
Затем на destinationVC (который должен принять описанный выше протокол) вы можете реализовать описанный метод func presentationControllerShouldDismiss(_ presentationController:) -> Bool или любой другой, чтобы правильно обрабатывать ваше пользовательское поведение.
Это должен быть принятый ответ. Он правильно решает проблему OP самым чистым способом. Я только что реализовал это, и это работает именно так, как вы ожидаете. Таким образом, вы также можете вернуть true или false на основе чего угодно.
Вместо того, чтобы устанавливать делегат в prepare(for:sender:) предыдущего контроллера представления, вы можете иметь presentationController?.delegate = self в viewDidLoad. Таким образом, весь код находится в одном контроллере представления.
В Apple Developer есть хорошо объясненный документ: developer.apple.com/documentation/uikit/view_controllers/…