Invoking Lifecycle Methods With iOS 13’s Modal Presentation
[caption id="attachment_426" align="aligncenter" width="700"]
Whoops, wrong lifecycle.[/caption]
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.
[caption id="attachment_428" align="aligncenter" width="1024"]
PageSheet on iPad Pro 12.9-inch.[/caption]
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:
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... UIAdaptivePresentationControllerDelegate (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.
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:
In the presented view controller’s ViewDidLoad method, set the navigationController’s presentation Controller delegate to self:
You can alternatively set this in the code that presents the presented view controller:
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.
[caption id="attachment_437" align="aligncenter" width="627"]
A nice view appears![/caption]
UIViewController to the rescue!
To do so, we can take advantage of the aforementioned beginAppearanceTransition and endAppearanceTransition.
These both take two parameters, isAppearing (whether 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:
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’s ViewWillAppear 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.
[caption id="" align="aligncenter" width="768"] Some cases with handles. Get it? It’s very funny.[/caption]