Last week, I needed to implement a resizing functionality for a modal displayed on iPad using the
formSheet modal presentation style. Basically,
We had a navigation controller presented modally, and needed a way to expand the modal to fill the screen or remain the default
formSheet size based on what View Controller was being displayed.
After lots of searching, it became clear to me that the only logical way to do this was to use a
custom modal presentation style instead, and
UIPresentationController. This seemed like a bit too much just to get the concept of resizing on a
formSheet modal. Also, by using a
custom presentation style,
we would have to give up the free functionality provided when a
formSheet is used. For example, when a keyboard is displayed, a modal presented using
formSheet adjusts it’s
origin.y, moving it to the top of the screen in order to ensure that most of the modal isn’t covered by the keyboard.
After investigating a bit further, I discovered two note-worthy facts:
- That it is possible to adjust the size of the
formSheetmodal by setting
preferredContentSizeof the View controller to be displayed before the modal is presented.
- And that when a new View Controller with a different
preferredContentSizeis presented in the same modal (In our case, we had a
UINavigationControlleras the container, and view controllers were pushed and popped as desired), the modal size doesn’t change, but if the device’s orientation changes, the size of the modal is then updated during the
layoutSubviews()pass triggered by the orientation change.
So all I needed to do was to find a way to trigger a layout pass from the right superView, and this would then cause the modal to resize using the
preferredContentSize of the new View Controller being displayed. This also had to be done in a maintainable way in case there was a need to support different sizes. This rest of this blog post covers the solution I came up with after a few iterations.
I’ve always been a big fan of separating presentation logic into a separate class. I normally call such classes
Routers. They are also commonly known as
Presenters. The main benefit of this particular separation of concern is the lack of presentation side effects in a View Controller. Also, the router can be separately tested in order to ensure that the correct View Controllers are displayed based on particular events. I choose to have this functionality in
the router. The reasoning here is that View Controllers shouldn’t care about what size they are presented in. For the most part, View Controllers should be adaptive to different size classes.
Breaking the solution into parts, we first want to have a means of detecting which viewControllers need to be resized, and then we need a way to resize said controllers. Enter
Resizing Policies to handle the first part.
We will define a resizing policy as a protocol that provides functionality for inferring whether a controller should be resized.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
In the snippet above, we have a
ModalResizingPolicy which conforms to
ResizingPolicy. This is where we will return the kind of size change that needs to occur for the current View Controller being displayed.
Now that we have a resizing policy, we need to use it to resize the View Controller in some way. With this in mind, we will define another protocol called
Resizer. Objects/structures conforming to this protocol must provide functionality for resizing a particular
ViewController given a particular
1 2 3 4 5 6 7 8 9
Putting it all together
ResizingPolicy and the
Resizer, we can now implement the intended functionality as illustrated below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
Router invokes the
resize function on it’s
resizer after the
navigationController has displayed the new top view controller. In the resize function, we first ensure that the viewController to be resized is actually contained in the navigation controller’s
viewControllers stack. We also hold on to the original corner radius for the form sheet. Doing this ensures that we use the same corner radius when moving from
fullScreen back to
formSheet size types. Finally, we animated the new content size change, by setting the
preferredContentSize of the
navigationController to the new size. I also noticed that setting
preferredContentSize doesn’t notify the system that a layout pass is needed, so this was done manually by invoking
This post was as a result of me unable to find a proper way to implement the functionality described above (via Google searching). Instead of implementing my own custom
PresentationController, this implementation gives the best of both worlds. I got to keep the features provided by the
formSheet presentation style, and was able to provide resizing functionality in a neat and self-contained manner. There are some minor kinks here that should be ironed out, like hiding the status bar when we go fullScreen or expanding the size of the navigation bar to account for the status bar, but this is beyond the scope of this blog post.
Thanks for taking the time 🙏🏿