Invoking Lifecycle Methods With iOS 13’s Modal Presentation
iOS 13 was legendary
iOS 13 brought many cool things; dark mode, sign in with Apple and Memoji, just to name a few.
One of my favourite changes was the new card-like modal presentation style, where the presenting view controller is still slightly visible and dimmed out behind the presented view controller, and the presented view controller can be dismissed by swiping it down.
Bugs, bugs, bugs
This new feature, however, introduced a few bugs to some codebases that depended on lifecycle methods (ie. ViewWillAppear
, ViewDidLoad
) to perform certain actions.
It’s not uncommon to come across a codebase that has some sort of logic in these lifecycle methods — analytics, or refreshing UI with new data, for example.
This would appear to be a great spot to do these sorts of things since, by definition, ViewWillAppear
gets called to notify the view controller that its view is about to be added to the view hierarchy.
There’s always a catch
With the new modal presentation, however, ViewWillAppear
won’t get called on the presenting view controller when the presented view controller is dismissed, because technically its view never left the view hierarchy.
What’s the workaround?
So how can we use this modal presentation style but also get these lifecycle calls?
One way to get around it is to use the completion block in dismiss to do the things you might’ve done in ViewWillAppear
.
This will not invoke ViewWillAppear
, but you could do something like:
dismiss(animated: true, completion: {
presentingVC.doStuff()
}
Why this isn’t my favorite solution
This is okay, but what if you don’t want to pass a reference of the presenting view controller? And what about the case where we don’t manually call dismiss, such as when a user swipes down to dismiss the modal?
Here’s something a little better
To find a solution where we don’t need to pass a reference of the presenting view controller and which also handles swipe gestures, we can take advantage of UIViewController’s
beginAppearanceTransition
and endAppearanceTransition
and UIAdaptivePresentationControllerDelegate’s
(rolls right off the tongue, right?)
What the hell is UIAdaptivePresentationControllerDelegate?
This delegate has been around, but two methods were recently added, presentationControllerWillDismiss
and presentationControllerDidDismiss
. These two get called when a user swipes to dismiss a modal view.
Which should we use?
presentationControllerWillDismiss
gets called every time a user starts swiping to dismiss the presented view controller. This means that it can get called multiple times if a user starts to swipe down, changes their mind, and then swipes back down again.
The winner
Because of this, presentationControllerDidDismiss
seems like the best place to write code that will manually invoke our presenting view controller’s ViewWillAppear
method.
So how do we use it then?
To intercept this event, we first make the presented view controller conform to UIAdaptivePresentationControllerDelegate
and implement the presentationControllerDidDismiss
method:
extension PresentedVC: UIAdaptivePresentationControllerDelegate { func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { // TO-DO: }}
In the presented view controller’s ViewDidLoad
method, set the navigationController
’s presentationController delegate to self:
override func viewDidLoad() {
super.viewDidLoad() self.navigationController.presentationController.delegate = self}
You can alternatively set this in the code that presents the presented view controller:
let presentedVC = PresentedVC()self.navigationController.presentationController.delegate = presentedVC
Time for some magic
Now that we can intercept the point when a user swipes to dismiss, we need to manually invoke ViewWillAppear
on the presenting view controller when the modal is dismissed.
UIViewController to the rescue!
To do so, we can take advantage of the aforementioned beginAppearanceTransition
and endAppearanceTransition
.
These both take two parameters, isAppearing
(wether or not the ViewController is coming into or out of the view hierarchy) and animated
.
Go back to our presentationControllerDidDismiss
implementation and add the following:
extension PresentedVC: UIAdaptivePresentationControllerDelegate {func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {// 1self.beginAppearanceTransition(isAppearing: false, animated: true)// 2self.presentingViewController.beginAppearanceTransition(isAppearing: true, animated: true)// 3self.endAppearanceTransition()self.presentingViewController.endAppearanceTransition()}}
Break it down now
Here’s what’s going down:
- Let it be known that self (presented view controllers view) will be leaving the view hierarchy
- Let it be known that
self.presentingViewController
(the presenting view controller’s view) will be “re-appearing” in the view hierarchy. This will ensure that the presenting view controller’sViewWillAppear
method gets called - Let it be known that both view controllers have finished their transition. Caution:
ViewWillAppear
will not get called if you forget to call end the appearance transition on either view controller because it will not know it has finished transitioning. (Don’t ask how I found that out)
Don’t forget to handle all cases!
This handles the case of invoking ViewWillAppear
on presenting view controller when the user swipes away the modal, but what if an action triggers the dismissal? For example, the user taps a cancel button on the navBar, or a delegate method is triggered on selection of something like a contact in CNContactViewController
?
Simply add the above begin and end appearanceTransition
methods in the appropriate places, such as your cancelTapped
or onContactValueSelected
methods, and you’re good to go.