检测 sheet 于 iOS 13 被驳回
Detecting sheet was dismissed on iOS 13
在iOS13之前,呈现的视图控制器用于覆盖整个屏幕。并且,当关闭时,父视图控制器 viewDidAppear
函数被执行。
现在 iOS 13 将默认将视图控制器显示为 sheet,这意味着卡片将部分覆盖底层视图控制器,这意味着 viewDidAppear
将不会被调用, 因为父视图控制器实际上从未消失过。
有没有办法检测到呈现的视图控制器sheet已被关闭?我可以在父视图控制器中覆盖其他一些功能 而不是使用某种委托?
Is there a way to detect that the presented view controller sheet was dismissed?
是的。
Some other function I can override in the parent view controller rather than using some sort of delegate?
没有。 "Some sort of delegate" 就是您的做法。让自己成为演示控制器的委托并覆盖 presentationControllerDidDismiss(_:)
.
缺少一个通用的 runtime-generated 事件来通知您显示的视图控制器(无论是否全屏)已被关闭,这确实很麻烦;但这不是一个新问题,因为一直有 non-fullscreen 个呈现的视图控制器。只是现在(在iOS13)多了!我在别处用单独的 question-and-answer 来讨论这个话题:.
对于未来的读者,这里有一个更完整的实现答案:
- 在准备 segue 的根视图控制器中添加以下内容(假设您的模态有一个导航控制器)
// Modal Dismiss iOS 13
modalNavController.presentationController?.delegate = modalVc
- 在模态视图控制器中添加以下委托+方法
// MARK: - iOS 13 Modal (Swipe to Dismiss)
extension ModalViewController: UIAdaptivePresentationControllerDelegate {
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
print("slide to dismiss stopped")
self.dismiss(animated: true, completion: nil)
}
}
- 确保在模态视图控制器中以下 属性 为真,以便调用委托方法
self.isModalInPresentation = true
- 利润
这是父视图控制器的代码示例,当子视图控制器呈现 作为 sheet(即在默认 iOS 13方式)被解雇:
public final class Parent: UIViewController, UIAdaptivePresentationControllerDelegate
{
// This is assuming that the segue is a storyboard segue;
// if you're manually presenting, just set the delegate there.
public override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
if segue.identifier == "mySegue" {
segue.destination.presentationController?.delegate = self;
}
}
public func presentationControllerDidDismiss(
_ presentationController: UIPresentationController)
{
// Only called when the sheet is dismissed by DRAGGING.
// You'll need something extra if you call .dismiss() on the child.
// (I found that overriding dismiss in the child and calling
// presentationController.delegate?.presentationControllerDidDismiss
// works well).
}
}
Jerland2 的回答很混乱,因为 (a) 最初的提问者想要在 sheet 被 dismissed 时获得函数调用(而他实现了 presentationControllerDidAttemptToDismiss,这是当用户尝试 并且失败 关闭 sheet 时调用),并且(b)设置 isModalInPresentation 是完全正交的,实际上将使呈现的 sheet 不可关闭(这与 OP 想要的相反)。
返回 viewWillAppear
和 viewDidAppear
的另一个选项已设置
let vc = UIViewController()
vc.modalPresentationStyle = .fullScreen
此选项覆盖全屏并在关闭后调用上述方法
调用presentingViewController.viewWillAppear不是很简单吗?
在解雇之前?
self.presentingViewController?.viewWillAppear(false)
self.dismiss(animated: true, completion: nil)
以我的观点,苹果不应该设置pageSheet
是默认的modalPresentationStyle
我想使用 swizzling
将 fullScreen
样式恢复为默认样式
像这样:
private func _swizzling(forClass: AnyClass, originalSelector: Selector, swizzledSelector: Selector) {
if let originalMethod = class_getInstanceMethod(forClass, originalSelector),
let swizzledMethod = class_getInstanceMethod(forClass, swizzledSelector) {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
extension UIViewController {
static func preventPageSheetPresentationStyle () {
UIViewController.preventPageSheetPresentation
}
static let preventPageSheetPresentation: Void = {
if #available(iOS 13, *) {
_swizzling(forClass: UIViewController.self,
originalSelector: #selector(present(_: animated: completion:)),
swizzledSelector: #selector(_swizzledPresent(_: animated: completion:)))
}
}()
@available(iOS 13.0, *)
private func _swizzledPresent(_ viewControllerToPresent: UIViewController,
animated flag: Bool,
completion: (() -> Void)? = nil) {
if viewControllerToPresent.modalPresentationStyle == .pageSheet
|| viewControllerToPresent.modalPresentationStyle == .automatic {
viewControllerToPresent.modalPresentationStyle = .fullScreen
}
_swizzledPresent(viewControllerToPresent, animated: flag, completion: completion)
}
}
然后将此行放入您的 AppDelegate
UIViewController.preventPageSheetPresentationStyle()
拖动或调用 DISMISS FUNC 将适用于以下代码。
1) 在根视图控制器中,你告诉它哪个是它的展示视图控制器,如下代码
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "presenterID" {
let navigationController = segue.destination as! UINavigationController
if #available(iOS 13.0, *) {
let controller = navigationController.topViewController as! presentationviewcontroller
// Modal Dismiss iOS 13
controller.presentationController?.delegate = self
} else {
// Fallback on earlier versions
}
navigationController.presentationController?.delegate = self
}
}
2) 再次在根视图控制器中,告诉您当它的展示视图控制器被取消时您将做什么
public func presentationControllerDidDismiss(
_ presentationController: UIPresentationController)
{
print("presentationControllerDidDismiss")
}
1) 在演示视图控制器中,当您点击此图片中的取消或保存按钮时。下面的代码将是 called.The
self.dismiss(animated: true) {
self.presentationController?.delegate?.presentationControllerDidDismiss?(self.presentationController!)
}
覆盖 UIViewController
上的 viewWillDisappear
被关闭。它会通过 isBeingDismissed
布尔标志提醒您解雇。
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if isBeingDismissed {
print("user is dismissing the vc")
}
}
** 如果用户在向下滑动卡片的中途再向上滑动卡片,即使卡片没有被关闭,它仍然会注册为被关闭。但这是您可能不关心的边缘情况。
Swift
在iOS13
中调用viewWillAppear
的一般解决方案
class ViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print("viewWillAppear")
}
//Show new viewController
@IBAction func show(_ sender: Any) {
let newViewController = NewViewController()
//set delegate of UIAdaptivePresentationControllerDelegate to self
newViewController.presentationController?.delegate = self
present(newViewController, animated: true, completion: nil)
}
}
extension UIViewController: UIAdaptivePresentationControllerDelegate {
public func presentationControllerDidDismiss( _ presentationController: UIPresentationController) {
if #available(iOS 13, *) {
//Call viewWillAppear only in iOS 13
viewWillAppear(true)
}
}
}
如果您在全屏模式下使用 ModalPresentationStyle,控制器的行为将恢复正常。
ConsultarController controllerConsultar = this.Storyboard.InstantiateViewController("ConsultarController") as ConsultarController;
controllerConsultar.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
this.NavigationController.PushViewController(controllerConsultar, true);
如果有人无法访问呈现的视图控制器,他们可以在呈现视图控制器中重写以下方法并将 modalPresentationStyle
更改为 fullScreen
或者可以添加其中一种策略上面提到的这种方法
override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
if let _ = viewControllerToPresent as? TargetVC {
viewControllerToPresent.modalPresentationStyle = .fullScreen
}
super.present(viewControllerToPresent, animated: flag, completion: completion)
}
如果呈现的视图控制器是导航控制器,而您想检查根控制器,可以将上述条件更改为
if let _ = (viewControllerToPresent as? UINavigationController)?.viewControllers.first as? TargetVC {
viewControllerToPresent.modalPresentationStyle = .fullScreen
}
如果您想在用户从 sheet 中关闭模式 sheet 时执行某些操作。
假设您已经有一些带有 @IBAction
的关闭按钮和在关闭或执行其他操作之前显示警报的逻辑。您只想检测用户按下此类控制器的时刻。
方法如下:
class MyModalSheetViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.presentationController?.delegate = self
}
@IBAction func closeAction(_ sender: Any) {
// your logic to decide to close or not, when to close, etc.
}
}
extension MyModalSheetViewController: UIAdaptivePresentationControllerDelegate {
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
return false // <-prevents the modal sheet from being closed
}
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
closeAction(self) // <- called after the modal sheet was prevented from being closed and leads to your own logic
}
}
在 SwiftUI 中你可以使用 onDismiss 闭包
func sheet<Item, Content>(item: Binding<Item?>, onDismiss: (() -> Void)?, content: (Item) -> Content) -> some View
在iOS13之前,呈现的视图控制器用于覆盖整个屏幕。并且,当关闭时,父视图控制器 viewDidAppear
函数被执行。
现在 iOS 13 将默认将视图控制器显示为 sheet,这意味着卡片将部分覆盖底层视图控制器,这意味着 viewDidAppear
将不会被调用, 因为父视图控制器实际上从未消失过。
有没有办法检测到呈现的视图控制器sheet已被关闭?我可以在父视图控制器中覆盖其他一些功能 而不是使用某种委托?
Is there a way to detect that the presented view controller sheet was dismissed?
是的。
Some other function I can override in the parent view controller rather than using some sort of delegate?
没有。 "Some sort of delegate" 就是您的做法。让自己成为演示控制器的委托并覆盖 presentationControllerDidDismiss(_:)
.
缺少一个通用的 runtime-generated 事件来通知您显示的视图控制器(无论是否全屏)已被关闭,这确实很麻烦;但这不是一个新问题,因为一直有 non-fullscreen 个呈现的视图控制器。只是现在(在iOS13)多了!我在别处用单独的 question-and-answer 来讨论这个话题:
对于未来的读者,这里有一个更完整的实现答案:
- 在准备 segue 的根视图控制器中添加以下内容(假设您的模态有一个导航控制器)
// Modal Dismiss iOS 13
modalNavController.presentationController?.delegate = modalVc
- 在模态视图控制器中添加以下委托+方法
// MARK: - iOS 13 Modal (Swipe to Dismiss)
extension ModalViewController: UIAdaptivePresentationControllerDelegate {
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
print("slide to dismiss stopped")
self.dismiss(animated: true, completion: nil)
}
}
- 确保在模态视图控制器中以下 属性 为真,以便调用委托方法
self.isModalInPresentation = true
- 利润
这是父视图控制器的代码示例,当子视图控制器呈现 作为 sheet(即在默认 iOS 13方式)被解雇:
public final class Parent: UIViewController, UIAdaptivePresentationControllerDelegate
{
// This is assuming that the segue is a storyboard segue;
// if you're manually presenting, just set the delegate there.
public override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
if segue.identifier == "mySegue" {
segue.destination.presentationController?.delegate = self;
}
}
public func presentationControllerDidDismiss(
_ presentationController: UIPresentationController)
{
// Only called when the sheet is dismissed by DRAGGING.
// You'll need something extra if you call .dismiss() on the child.
// (I found that overriding dismiss in the child and calling
// presentationController.delegate?.presentationControllerDidDismiss
// works well).
}
}
Jerland2 的回答很混乱,因为 (a) 最初的提问者想要在 sheet 被 dismissed 时获得函数调用(而他实现了 presentationControllerDidAttemptToDismiss,这是当用户尝试 并且失败 关闭 sheet 时调用),并且(b)设置 isModalInPresentation 是完全正交的,实际上将使呈现的 sheet 不可关闭(这与 OP 想要的相反)。
返回 viewWillAppear
和 viewDidAppear
的另一个选项已设置
let vc = UIViewController()
vc.modalPresentationStyle = .fullScreen
此选项覆盖全屏并在关闭后调用上述方法
调用presentingViewController.viewWillAppear不是很简单吗? 在解雇之前?
self.presentingViewController?.viewWillAppear(false)
self.dismiss(animated: true, completion: nil)
以我的观点,苹果不应该设置pageSheet
是默认的modalPresentationStyle
我想使用 swizzling
fullScreen
样式恢复为默认样式
像这样:
private func _swizzling(forClass: AnyClass, originalSelector: Selector, swizzledSelector: Selector) {
if let originalMethod = class_getInstanceMethod(forClass, originalSelector),
let swizzledMethod = class_getInstanceMethod(forClass, swizzledSelector) {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
extension UIViewController {
static func preventPageSheetPresentationStyle () {
UIViewController.preventPageSheetPresentation
}
static let preventPageSheetPresentation: Void = {
if #available(iOS 13, *) {
_swizzling(forClass: UIViewController.self,
originalSelector: #selector(present(_: animated: completion:)),
swizzledSelector: #selector(_swizzledPresent(_: animated: completion:)))
}
}()
@available(iOS 13.0, *)
private func _swizzledPresent(_ viewControllerToPresent: UIViewController,
animated flag: Bool,
completion: (() -> Void)? = nil) {
if viewControllerToPresent.modalPresentationStyle == .pageSheet
|| viewControllerToPresent.modalPresentationStyle == .automatic {
viewControllerToPresent.modalPresentationStyle = .fullScreen
}
_swizzledPresent(viewControllerToPresent, animated: flag, completion: completion)
}
}
然后将此行放入您的 AppDelegate
UIViewController.preventPageSheetPresentationStyle()
拖动或调用 DISMISS FUNC 将适用于以下代码。
1) 在根视图控制器中,你告诉它哪个是它的展示视图控制器,如下代码
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "presenterID" {
let navigationController = segue.destination as! UINavigationController
if #available(iOS 13.0, *) {
let controller = navigationController.topViewController as! presentationviewcontroller
// Modal Dismiss iOS 13
controller.presentationController?.delegate = self
} else {
// Fallback on earlier versions
}
navigationController.presentationController?.delegate = self
}
}
2) 再次在根视图控制器中,告诉您当它的展示视图控制器被取消时您将做什么
public func presentationControllerDidDismiss(
_ presentationController: UIPresentationController)
{
print("presentationControllerDidDismiss")
}
1) 在演示视图控制器中,当您点击此图片中的取消或保存按钮时。下面的代码将是 called.The
self.dismiss(animated: true) {
self.presentationController?.delegate?.presentationControllerDidDismiss?(self.presentationController!)
}
覆盖 UIViewController
上的 viewWillDisappear
被关闭。它会通过 isBeingDismissed
布尔标志提醒您解雇。
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if isBeingDismissed {
print("user is dismissing the vc")
}
}
** 如果用户在向下滑动卡片的中途再向上滑动卡片,即使卡片没有被关闭,它仍然会注册为被关闭。但这是您可能不关心的边缘情况。
Swift
在iOS13
中调用viewWillAppear
的一般解决方案
class ViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print("viewWillAppear")
}
//Show new viewController
@IBAction func show(_ sender: Any) {
let newViewController = NewViewController()
//set delegate of UIAdaptivePresentationControllerDelegate to self
newViewController.presentationController?.delegate = self
present(newViewController, animated: true, completion: nil)
}
}
extension UIViewController: UIAdaptivePresentationControllerDelegate {
public func presentationControllerDidDismiss( _ presentationController: UIPresentationController) {
if #available(iOS 13, *) {
//Call viewWillAppear only in iOS 13
viewWillAppear(true)
}
}
}
如果您在全屏模式下使用 ModalPresentationStyle,控制器的行为将恢复正常。
ConsultarController controllerConsultar = this.Storyboard.InstantiateViewController("ConsultarController") as ConsultarController; controllerConsultar.ModalPresentationStyle = UIModalPresentationStyle.FullScreen; this.NavigationController.PushViewController(controllerConsultar, true);
如果有人无法访问呈现的视图控制器,他们可以在呈现视图控制器中重写以下方法并将 modalPresentationStyle
更改为 fullScreen
或者可以添加其中一种策略上面提到的这种方法
override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
if let _ = viewControllerToPresent as? TargetVC {
viewControllerToPresent.modalPresentationStyle = .fullScreen
}
super.present(viewControllerToPresent, animated: flag, completion: completion)
}
如果呈现的视图控制器是导航控制器,而您想检查根控制器,可以将上述条件更改为
if let _ = (viewControllerToPresent as? UINavigationController)?.viewControllers.first as? TargetVC {
viewControllerToPresent.modalPresentationStyle = .fullScreen
}
如果您想在用户从 sheet 中关闭模式 sheet 时执行某些操作。
假设您已经有一些带有 @IBAction
的关闭按钮和在关闭或执行其他操作之前显示警报的逻辑。您只想检测用户按下此类控制器的时刻。
方法如下:
class MyModalSheetViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.presentationController?.delegate = self
}
@IBAction func closeAction(_ sender: Any) {
// your logic to decide to close or not, when to close, etc.
}
}
extension MyModalSheetViewController: UIAdaptivePresentationControllerDelegate {
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
return false // <-prevents the modal sheet from being closed
}
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
closeAction(self) // <- called after the modal sheet was prevented from being closed and leads to your own logic
}
}
在 SwiftUI 中你可以使用 onDismiss 闭包
func sheet<Item, Content>(item: Binding<Item?>, onDismiss: (() -> Void)?, content: (Item) -> Content) -> some View