SFSafariViewController not dismissing properly (iOS 10.3) - ios

In my app I have a SFSafariViewController that I am displaying modally. Upon dismissal, the presenting ViewController does not have its dismiss method called. Code for my subclass of UIViewController:
override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
print("will present")
super.present(viewControllerToPresent, animated: flag) {
completion?()
print("did present")
}
}
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
print("will dismiss)")
super.dismiss(animated: flag) {
completion?()
print("did dismiss")
}
}
func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
print("finished with \(controller)")
}
func testSVC() {
let svc = SFSafariViewController(url: URL(string: "https://www.stackoverflow.com")!)
svc.delegate = self
self.present(svc, animated: true) {
print("presented \(svc)")
}
}
Calling testSVC() and then tapping "Done" in the SafariViewController produces the following output:
will present
presented <SFSafariViewController: 0x7fd981b2f200>
did present
finished with <SFSafariViewController: 0x7fd981b2f200>
And that's it. The dismiss print statements are missing. Can anyone help me figure out why dismiss is not being called? I thought that all UIViewController dismissals were forwarded to the presenting UIViewController.

When you dismiss the SFSafariViewController the dismiss method for svc is fired not the ViewController you're in. If you want to override the dismiss for the SFSafariViewController then you should subclass it and override the dismiss func in that subclass.

Related

interactivePopGestureRecognizer pop to root instead of 1 top controller

I use default implementation of UINavigationController with default gesture for popping left to right (interactivePopGestureRecognizer). How can I make interactivePopGestureRecognizer popping to root controller instead of only 1 top controller?
I found a way to just delete previous viewController from stack after pushing a new one.
navigationController.pushViewController(newViewController, animated: true, completion: {
self.navigationController.removePreviousViewController()
})
A here is an extension
extension UINavigationController {
func pushViewController(_ viewController: UIViewController, animated: Bool, completion: #escaping () -> Void) {
pushViewController(viewController, animated: animated)
guard animated, let coordinator = transitionCoordinator else {
DispatchQueue.main.async { completion() }
return
}
coordinator.animate(alongsideTransition: nil) { _ in completion() }
}
func removePreviousViewController() {
if viewControllers.count > 2 {
viewControllers.removePrevious()
}
}
}
Some helpers
extension Array {
mutating func removePrevious() {
remove(at: count - 2)
}
}

Detecting sheet was dismissed on iOS 13

Before iOS 13, presented view controllers used to cover the entire screen. And, when dismissed, the parent view controller viewDidAppear function were executed.
Now iOS 13 will present view controllers as a sheet as default, which means the card will partially cover the underlying view controller, which means that viewDidAppear will not be called, because the parent view controller has never actually disappeared.
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?
Is there a way to detect that the presented view controller sheet was dismissed?
Yes.
Some other function I can override in the parent view controller rather than using some sort of delegate?
No. "Some sort of delegate" is how you do it. Make yourself the presentation controller's delegate and override presentationControllerDidDismiss(_:).
https://developer.apple.com/documentation/uikit/uiadaptivepresentationcontrollerdelegate/3229889-presentationcontrollerdiddismiss
The lack of a general runtime-generated event informing you that a presented view controller, whether fullscreen or not, has been dismissed, is indeed troublesome; but it's not a new issue, because there have always been non-fullscreen presented view controllers. It's just that now (in iOS 13) there are more of them! I devote a separate question-and-answer to this topic elsewhere: Unified UIViewController "became frontmost" detection?.
Here's a code example of a parent view-controller which is notified when the child view-controller it presents as a sheet (i.e., in the default iOS 13 manner) is dismissed:
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's answer is confused, since (a) the original questioner wanted to get a function call when the sheet is dismissed (whereas he implemented presentationControllerDidAttemptToDismiss, which is called when the user tries and fails to dismiss the sheet), and (b) setting isModalInPresentation is entirely orthogonal and in fact will make the presented sheet undismissable (which is the opposite of what OP wants).
For future readers here is a more complete answer with implementation:
In the root view controllers prepare for segue add the following (Assuming your modal has a nav controller)
// Modal Dismiss iOS 13
modalNavController.presentationController?.delegate = modalVc
In the modal view controller add the following delegate + method
// 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)
}
}
Ensure in the modal View Controller that the following property is true in order for the delegate method to be called
self.isModalInPresentation = true
Profit
Another option to get back viewWillAppear and viewDidAppear is set
let vc = UIViewController()
vc.modalPresentationStyle = .fullScreen
this option cover full screen and after dismiss, calls above methods
Swift
General Solution to call viewWillAppear in iOS13
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)
}
}
}
If you want to do something when user closes the modal sheet from within that sheet.
Let's assume you already have some Close button with an #IBAction and a logic to show an alert before closing or do something else. You just want to detect the moment when user makes push down on such a controller.
Here's how:
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
}
}
Override viewWillDisappear on the UIViewController that's being dismissed. It will alert you to a dismissal via isBeingDismissed boolean flag.
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if isBeingDismissed {
print("user is dismissing the vc")
}
}
** If the user is halfway through the swipe down and swipes the card back up, it'll still register as being dismissed, even if the card is not dismissed. But that's an edge case you may not care about.
DRAG OR CALL DISMISS FUNC will work with below code.
1) In root view controller, you tell that which is its presentation view controller as below code
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) Again in the root view controller, you tell what you will do when its presentation view controller is dissmised
public func presentationControllerDidDismiss(
_ presentationController: UIPresentationController)
{
print("presentationControllerDidDismiss")
}
1) In the presentation view controller, When you hit cancel or save button in this picture. Below code will be called.The
self.dismiss(animated: true) {
self.presentationController?.delegate?.presentationControllerDidDismiss?(self.presentationController!)
}
in SwiftUI you can use onDismiss closure
func sheet<Item, Content>(item: Binding<Item?>, onDismiss: (() -> Void)?, content: (Item) -> Content) -> some View
If someone doesn't have access to the presented view controller, they can just override the following method in presenting view controller and change the modalPresentationStyle to fullScreen or can add one of the strategies mentioned above with this approach
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 presented view controller is navigation controller and you want to check the root controller, can change the above condition to be like
if let _ = (viewControllerToPresent as? UINavigationController)?.viewControllers.first as? TargetVC {
viewControllerToPresent.modalPresentationStyle = .fullScreen
}
If you used the ModalPresentationStyle in FullScreen, the behavior of the controller is back as usual.
ConsultarController controllerConsultar = this.Storyboard.InstantiateViewController("ConsultarController") as ConsultarController;
controllerConsultar.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
this.NavigationController.PushViewController(controllerConsultar, true);
From my point of view, Apple should not set pageSheet is the default modalPresentationStyle
I'd like to bring fullScreen style back to default by using swizzling
Like this:
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)
}
}
And then put this line to your AppDelegate
UIViewController.preventPageSheetPresentationStyle()
wouldn't it be simple to call the presentingViewController.viewWillAppear?
befor dismissing?
self.presentingViewController?.viewWillAppear(false)
self.dismiss(animated: true, completion: nil)

Admob Rewarded video - Root view controller issue

So I have two view controllers in my application:
StartViewController (Root view controller) and GameViewController
I am presenting my Rewarded video from GameViewController
Everything works perfectly, except for when the user presses "Skip now" on the rewarded video. It dismisses GameViewController and goes back to StartViewController which is my root view controller.
If the user watches the entire video, it works as intended.
The code for presenting rewarded view from GameViewController:
func playReward()
{
if rewardVideo!.isReady
{
if var topController = UIApplication.shared.keyWindow?.rootViewController
{
while let presentedViewController = topController.presentedViewController
{
// Make top controller topmost view controller
topController = presentedViewController
}
rewardVideo!.present(fromRootViewController: topController)
}
}
}
I temporarily changed the root to GameViewController to see if this was the issue and doing so fixed it, so I know it is an issue related to the root view controller and the "Skip now" button on the rewarded video.
I had the same issue. I solved it by overriding the func dismiss(animated flag: Bool, completion: (() -> Void)? = nil method.
Here is what I did.
var didOpenRewardedVideo:Int = 0
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
if didOpenRewardedVideo == 1 {
didOpenRewardedVideo = 2
super.dismiss(animated: flag, completion: completion)
}
else if didOpenRewardedVideo == 2{
didOpenRewardedVideo = 0
}
else{
super.dismiss(animated: flag, completion: completion)
}
}
func showRewardedVideo()
{
didOpenRewardedVideo = 1
GADRewardBasedVideoAd.sharedInstance().present(fromRootViewController: self)
}
Before showing the rewardedAd, do not forget to check if it's ready or not.
GADRewardBasedVideoAd.sharedInstance().isReady == true
When rewardedAd is presented didOpenRewardedVideo is set to 1. When user dismissing rewardedAd didOpenRewardedVideo is 1 and calling super.dismiss(animated: flag, completion: completion). Then didOpenRewardedVideo is set to 2. Now I know dismiss(animated flag: Bool, completion: (() -> Void)? = nil) will be called once more. This time I don't call the super method and set didOpenRewardedVideo to 0. I know if I dismiss my UIViewController will be dismissed.
#mialkan,
your solution does not work. I don't why no one talk about this issue. I am also facing such problem.
import GoogleMobileAds import sdk in your class or viewcontroller
GADRewardBasedVideoAdDelegate Add this in your class or viewcontroller
var RewardBasedVideo: GADRewardBasedVideoAd? **initialize AdController object **
override func viewDidLoad()
{
super.viewDidLoad()
RewardBasedVideo=GADRewardBasedVideoAd.sharedInstance()
RewardBasedVideo?.delegate = self
}
//MARK:- WATCH AD BUTTON CLICK
#IBAction func WatchAdBtn_Click(_ sender: UIButton)
{
if RewardBasedVideo?.isReady == true
{
RewardBasedVideo?.present(fromRootViewController: self)
} else
{
//Show alert here "Ads is not ready to load"
}
}
func rewardBasedVideoAdDidClose(_ rewardBasedVideoAd: GADRewardBasedVideoAd)
{
print("Reward based video ad is closed.")
}
For those of you who is facing the the same issue:
You can create a new class for your rootViewController (TabBarController or NavigationController etc.) and implement there something like that:
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
dismissalCounter += 1
if (dismissalCounter < 2) {
super.dismiss(animated: flag, completion: completion)
}
}
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
dismissalCounter = 0
}
override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
dismissalCounter = 0
super.present(viewControllerToPresent, animated: flag, completion: completion)
}
var dismissalCounter : Int = 0
Important! Use this functions inside TabBarController or NavigationController, otherwise it is not gonna work
UPD:
In my case unfortunatly it breaks all NavigationControllers inside a TabBarController (titles don't show and there are no buttons inside them), if I will figure fix actions, I'll let you know
UPD2:
Pretty obvious decision will be to change the initialViewController and view add from it, it'll not be dismissed
UPD3:
I solved this very and very strange:
class ViewController : UIViewController {
override func viewWillAppear(_ animated: Bool) {
if GADRewardBasedVideoAd.sharedInstance().isReady == false {
let request = GADRequest()
rewardBasedVideo!.load(request, withAdUnitID: "ca-app-pub-3940256099942544/1712485313")
}
}
var rewardBasedVideo: GADRewardBasedVideoAd?
#IBAction func ad_button_click(_ sender: Any) {
if rewardBasedVideo!.isReady == true {
let bl = blur()
self.present(bl, animated: true, completion: {
self.rewardBasedVideo?.present(fromRootViewController: bl)
})
}
}
}
class blur : UIViewController {
override func viewDidLoad() {
checkForKeyWindow()
}
func checkForKeyWindow() {
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: {
if (UIApplication.topViewController() == self) {
print("dismissed and forgotten")
self.dismiss(animated: true, completion: nil)
} else {
print("not keywindow")
self.checkForKeyWindow()
}
})
}
#objc func close() {
self.dismiss(animated: true, completion: nil)
}
}
extension UIApplication {
class func topViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController {
let moreNavigationController = tab.moreNavigationController
if let top = moreNavigationController.topViewController, top.view.window != nil {
return topViewController(base: top)
} else if let selected = tab.selectedViewController {
return topViewController(base: selected)
}
}
if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
return base
}
}

Swift: Not able to dismiss modally presented LoginViewController

As SplitViewController loads, I am showing a Login Screen. On successful login, I need to go back to parent view controller. Somehow dismissal is not working for me. Here is the code:
ParentViewController:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
if !appDelegate.loggedIn {
self.performSegueWithIdentifier("loginScreen", sender: self)
}
}
override func viewDidLoad() {
super.viewDidLoad()
}
Child ViewController:
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.loggedIn = true
self.dismissViewControllerAnimated(true, completion: nil)
The dismissal part never works. It just hangs on Login Screen.
Try one of the following:
1) remove self. keep only dismissViewControllerAnimated(true, completion: nil)
or remove self. and make it:
2) presentingViewController.dismissViewControllerAnimated(true, completion: nil)
or remove self. and try:
3) presentedViewController.dismissViewControllerAnimated(true, completion: nil)
Try this in your parent view controller:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
if !appDelegate.loggedIn {
let loginVC: UIViewController = self.storyboard!.instantiateViewControllerWithIdentifier("LoginViewController") as UIViewController
loginVC = UIModalTransitionStyle.CoverVertical
self.parentViewController?.presentViewController(loginVC, animated: true, completion: nil)
}
}
You're instantiating the new view controller by its own name rather than by the segue name.

Completion block for popViewController

When dismissing a modal view controller using dismissViewController, there is the option to provide a completion block. Is there a similar equivalent for popViewController?
The completion argument is quite handy. For instance, I can use it to hold off removing a row from a tableview until the modal is off screen, letting the user see the row animation. When returning from a pushed view controller, I would like the same opportunity.
I have tried placing popViewController in an UIView animation block, where I do have access to a completion block. However, this produces some unwanted side effects on the view being popped to.
If there is no such method available, what are some workarounds?
I know an answer has been accepted over two years ago, however this answer is incomplete.
There is no way to do what you're wanting out-of-the-box
This is technically correct because the UINavigationController API doesn't offer any options for this. However by using the CoreAnimation framework it's possible to add a completion block to the underlying animation:
[CATransaction begin];
[CATransaction setCompletionBlock:^{
// handle completion here
}];
[self.navigationController popViewControllerAnimated:YES];
[CATransaction commit];
The completion block will be called as soon as the animation used by popViewControllerAnimated: ends. This functionality has been available since iOS 4.
Swift 5 version - works like a charm. Based on this answer
extension UINavigationController {
func pushViewController(viewController: UIViewController, animated: Bool, completion: #escaping () -> Void) {
pushViewController(viewController, animated: animated)
if animated, let coordinator = transitionCoordinator {
coordinator.animate(alongsideTransition: nil) { _ in
completion()
}
} else {
completion()
}
}
func popViewController(animated: Bool, completion: #escaping () -> Void) {
popViewController(animated: animated)
if animated, let coordinator = transitionCoordinator {
coordinator.animate(alongsideTransition: nil) { _ in
completion()
}
} else {
completion()
}
}
}
I made a Swift version with extensions with #JorisKluivers answer.
This will call a completion closure after the animation is done for both push and pop.
extension UINavigationController {
func popViewControllerWithHandler(completion: ()->()) {
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
self.popViewControllerAnimated(true)
CATransaction.commit()
}
func pushViewController(viewController: UIViewController, completion: ()->()) {
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
self.pushViewController(viewController, animated: true)
CATransaction.commit()
}
}
SWIFT 4.1
extension UINavigationController {
func pushToViewController(_ viewController: UIViewController, animated:Bool = true, completion: #escaping ()->()) {
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
self.pushViewController(viewController, animated: animated)
CATransaction.commit()
}
func popViewController(animated:Bool = true, completion: #escaping ()->()) {
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
self.popViewController(animated: animated)
CATransaction.commit()
}
func popToViewController(_ viewController: UIViewController, animated:Bool = true, completion: #escaping ()->()) {
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
self.popToViewController(viewController, animated: animated)
CATransaction.commit()
}
func popToRootViewController(animated:Bool = true, completion: #escaping ()->()) {
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
self.popToRootViewController(animated: animated)
CATransaction.commit()
}
}
Working with or without animation properly, and also includes popToRootViewController:
// updated for Swift 3.0
extension UINavigationController {
private func doAfterAnimatingTransition(animated: Bool, completion: #escaping (() -> Void)) {
if let coordinator = transitionCoordinator, animated {
coordinator.animate(alongsideTransition: nil, completion: { _ in
completion()
})
} else {
DispatchQueue.main.async {
completion()
}
}
}
func pushViewController(viewController: UIViewController, animated: Bool, completion: #escaping (() -> Void)) {
pushViewController(viewController, animated: animated)
doAfterAnimatingTransition(animated: animated, completion: completion)
}
func popViewController(animated: Bool, completion: #escaping (() -> Void)) {
popViewController(animated: animated)
doAfterAnimatingTransition(animated: animated, completion: completion)
}
func popToRootViewController(animated: Bool, completion: #escaping (() -> Void)) {
popToRootViewController(animated: animated)
doAfterAnimatingTransition(animated: animated, completion: completion)
}
}
I had the same issue. And because I had to use it in multiple occasions, and within chains of completion blocks, I created this generic solution in an UINavigationController subclass:
- (void) navigationController:(UINavigationController *) navigationController didShowViewController:(UIViewController *) viewController animated:(BOOL) animated {
if (_completion) {
dispatch_async(dispatch_get_main_queue(),
^{
_completion();
_completion = nil;
});
}
}
- (UIViewController *) popViewControllerAnimated:(BOOL) animated completion:(void (^)()) completion {
_completion = completion;
return [super popViewControllerAnimated:animated];
}
Assuming
#interface NavigationController : UINavigationController <UINavigationControllerDelegate>
and
#implementation NavigationController {
void (^_completion)();
}
and
- (id) initWithRootViewController:(UIViewController *) rootViewController {
self = [super initWithRootViewController:rootViewController];
if (self) {
self.delegate = self;
}
return self;
}
Based on #HotJard's answer, when all you want is just a couple of lines of code. Quick and Easy.
Swift 4:
_ = self.navigationController?.popViewController(animated: true)
self.navigationController?.transitionCoordinator.animate(alongsideTransition: nil) { _ in
doWhatIWantAfterContollerHasPopped()
}
There is no way to do what you're wanting out-of-the-box. i.e. there is no method with a completion block for popping a view controller from a nav stack.
What I would do is put the logic in viewDidAppear. That will be called when the view has finished coming on screen. It'll be called for all different scenarios of the view controller appearing, but that should be fine.
Or you could use the UINavigationControllerDelegate method navigationController:didShowViewController:animated: to do a similar thing. This is called when the navigation controller has finished pushing or popping a view controller.
For 2018 ...
if you have this ...
navigationController?.popViewController(animated: false)
// I want this to happen next, help! ->
nextStep()
and you want to add a completion ...
CATransaction.begin()
navigationController?.popViewController(animated: true)
CATransaction.setCompletionBlock({ [weak self] in
self?.nextStep() })
CATransaction.commit()
it's that simple.
Handy tip...
It's the same deal for the handy popToViewController call.
A typical thing is you have an onboarding stack of a zillion screens. When finally done, you go all the way back to your "base" screen, and then finally fire up the app.
So in the "base" screen, to go "all the way back", popToViewController(self
func onboardingStackFinallyComplete() {
CATransaction.begin()
navigationController?.popToViewController(self, animated: false)
CATransaction.setCompletionBlock({ [weak self] in
guard let self = self else { return }
.. actually launch the main part of the app
})
CATransaction.commit()
}
Cleaned up Swift 4 version based on this answer.
extension UINavigationController {
func pushViewController(_ viewController: UIViewController, animated: Bool, completion: #escaping () -> Void) {
self.pushViewController(viewController, animated: animated)
self.callCompletion(animated: animated, completion: completion)
}
func popViewController(animated: Bool, completion: #escaping () -> Void) -> UIViewController? {
let viewController = self.popViewController(animated: animated)
self.callCompletion(animated: animated, completion: completion)
return viewController
}
private func callCompletion(animated: Bool, completion: #escaping () -> Void) {
if animated, let coordinator = self.transitionCoordinator {
coordinator.animate(alongsideTransition: nil) { _ in
completion()
}
} else {
completion()
}
}
}
The completion block is called after the viewDidDisappear method is called on the presented view controller, So putting code in the viewDidDisappear method of the popped view controller should work the same as a completion block.
Swift 3 answer, thanks to this answer: https://stackoverflow.com/a/28232570/3412567
//MARK:UINavigationController Extension
extension UINavigationController {
//Same function as "popViewController", but allow us to know when this function ends
func popViewControllerWithHandler(completion: #escaping ()->()) {
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
self.popViewController(animated: true)
CATransaction.commit()
}
func pushViewController(viewController: UIViewController, completion: #escaping ()->()) {
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
self.pushViewController(viewController, animated: true)
CATransaction.commit()
}
}
Swift 4 version with optional viewController parameter to pop to a specific one.
extension UINavigationController {
func pushViewController(viewController: UIViewController, animated:
Bool, completion: #escaping () -> ()) {
pushViewController(viewController, animated: animated)
if let coordinator = transitionCoordinator, animated {
coordinator.animate(alongsideTransition: nil) { _ in
completion()
}
} else {
completion()
}
}
func popViewController(viewController: UIViewController? = nil,
animated: Bool, completion: #escaping () -> ()) {
if let viewController = viewController {
popToViewController(viewController, animated: animated)
} else {
popViewController(animated: animated)
}
if let coordinator = transitionCoordinator, animated {
coordinator.animate(alongsideTransition: nil) { _ in
completion()
}
} else {
completion()
}
}
}
Please refer to recent version(5.1) of Swifty & SDK-like way,
extension UINavigationController {
func popViewController(animated: Bool, completion: (() -> ())? = nil) {
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
popViewController(animated: animated)
CATransaction.commit()
}
func pushViewController(_ viewController: UIViewController, animated: Bool, completion: (() -> ())? = nil) {
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
pushViewController(viewController, animated: animated)
CATransaction.commit()
}
}
2020 Swift 5.1 way
This solution guarantee that completion is executed after popViewController is fully finished. You can test it by doing another operation on the NavigationController in completion: In all other solutions above the UINavigationController is still busy with popViewController operation and does not respond.
public class NavigationController: UINavigationController, UINavigationControllerDelegate
{
private var completion: (() -> Void)?
override init(rootViewController: UIViewController) {
super.init(rootViewController: rootViewController)
delegate = self
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool)
{
if self.completion != nil {
DispatchQueue.main.async(execute: {
self.completion?()
self.completion = nil
})
}
}
func popViewController(animated: Bool, completion: #escaping () -> Void) -> UIViewController?
{
self.completion = completion
return super.popViewController(animated: animated)
}
}
Just for completeness, I've made an Objective-C category ready to use:
// UINavigationController+CompletionBlock.h
#import <UIKit/UIKit.h>
#interface UINavigationController (CompletionBlock)
- (UIViewController *)popViewControllerAnimated:(BOOL)animated completion:(void (^)()) completion;
#end
// UINavigationController+CompletionBlock.m
#import "UINavigationController+CompletionBlock.h"
#implementation UINavigationController (CompletionBlock)
- (UIViewController *)popViewControllerAnimated:(BOOL)animated completion:(void (^)()) completion {
[CATransaction begin];
[CATransaction setCompletionBlock:^{
completion();
}];
UIViewController *vc = [self popViewControllerAnimated:animated];
[CATransaction commit];
return vc;
}
#end
There is a pod called UINavigationControllerWithCompletionBlock which adds support for a completion block when both pushing and popping on a UINavigationController.
Use the next extension on your code: (Swift 4)
import UIKit
extension UINavigationController {
func popViewController(animated: Bool = true, completion: #escaping () -> Void) {
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
popViewController(animated: animated)
CATransaction.commit()
}
func pushViewController(_ viewController: UIViewController, animated: Bool = true, completion: #escaping () -> Void) {
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
pushViewController(viewController, animated: animated)
CATransaction.commit()
}
}
I achieved exactly this with precision using a block. I wanted my fetched results controller to show the row that was added by the modal view, only once it had fully left the screen, so the user could see the change happening. In prepare for segue which is responsible for showing the modal view controller, I set the block I want to execute when the modal disappears. And in the modal view controller I override viewDidDissapear and then call the block. I simply begin updates when the modal is going to appear and end updates when it disappears, but that is because I'm using a NSFetchedResultsController however you can do whatever you like inside the block.
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
if([segue.identifier isEqualToString:#"addPassword"]){
UINavigationController* nav = (UINavigationController*)segue.destinationViewController;
AddPasswordViewController* v = (AddPasswordViewController*)nav.topViewController;
...
// makes row appear after modal is away.
[self.tableView beginUpdates];
[v setViewDidDissapear:^(BOOL animated) {
[self.tableView endUpdates];
}];
}
}
#interface AddPasswordViewController : UITableViewController<UITextFieldDelegate>
...
#property (nonatomic, copy, nullable) void (^viewDidDissapear)(BOOL animated);
#end
#implementation AddPasswordViewController{
...
-(void)viewDidDisappear:(BOOL)animated{
[super viewDidDisappear:animated];
if(self.viewDidDissapear){
self.viewDidDissapear(animated);
}
}
#end
I found that the implementation of func navigationController(UINavigationController, didShow: UIViewController, animated: Bool) is the only working solution here.
We may make it better using RxSwift:
import UIKit
import RxSwift
import RxCocoa
extension Reactive where Base: UINavigationController {
func popToViewController(_ viewController: UIViewController, animated: Bool) -> ControlEvent<ShowEvent> {
let source = didShow
.filter { [weak viewController] event in
viewController == event.0
}
.take(1)
_ = base.popToViewController(viewController, animated: animated)
return ControlEvent(events: source)
}
}
Usage:
// let navigationController = UINavigationController(rootViewController: page1)
// navigationController.pushViewController(page2, animated: false)
navigationController.rx
.popToViewController(page1, animated: true)
.bind { _ in
// pop completion
}
.disposed(by: disposeBag)
I think viewDidDisappear(_ animated: Bool) function can help for this. It will be called when the view did disappeared completely.
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
//do the stuff here
}

Resources