Thinking Out Loud

Logical Ownership of UIViewController Presentations || Button, button, who’s got the button?

Thu, Mar 19, 2015
  1. ios-dev
  2. uiviewcontroller
  3. presentation

Prior to iOS 8, view controller presentation worked like so:

class ViewControllerA: UIViewController { 
	func presentTheThing() {
		let theThing = ViewControllerB()
		theThing.modalPresentationStyle = .SomeStyle
		self.presentViewController(theThing, animated: true, completion: nil)
	}
}

Here’s my problem with this. Who’s job is it to make sure that theThing is dismissible? Traditionally, if the modalPresentationStyle required a dismiss button (basically any presentation style other than popover requires a way to dismiss the presentedViewController), it was up to the presenting view controller to shove the view controller to be presented into a UINavigationController so that a dismiss button could be added to the top bar. This works fine when your presenting content view controllers, but quickly falls down in other situations. What if the view controller to be presented was already a UINavigationController? Or what if the view controller to be presented was a custom container. Who is responsible then for making sure the dismiss button is actually there? Before today, I would have argued that to comply with Encapsulation (one of the major pillars of OO design) the view controller being presented should know how to style itself for any presentation. The alternative would be that the view controller doing the presenting would have to know way too much about the view controller it’s about present. With that mindset, the custom container (which has it’s own navigation bar, by the way) should know when it should add a dismiss button to its navigation bar and when it shouldn’t depending on how it’s currently being presented. Unfortunately, there really isn’t any API to figure that out.

Let’s move on to iOS 8 for now…

With iOS 8 Apple added UIPresentationController into the mix. No longer are the presentingViewController and presentedViewController the only ones involved in presentation. Now, the view controller to be presented will vend a presentationController (or popoverPresentationController as appropriate) so that the presentation can participate in adaptability. The presentationController also allows a delegate to tweak how adaptability is handled. The two delegate callbacks that we care about for this discussion are:

// Allows the delegate to change the presentation style from its default adaptive alternitive
adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle

// Allows the delegate to change the view controller hierarchy for a given adaptive presentation style, usually this means shoving the presentedViewController into a UINavigationController
presentationController(controller: UIPresentationController, viewControllerForAdaptivePresentationStyle style: UIModalPresentationStyle) -> UIViewController?

So, back to my custom container, still in the Encapsulation mindset…

My initial thought was that my custom container could become the delegate of its presentationController and when it got the presentationController(_,viewControllerForAdaptivePresentationStyle:) call, instead of wrapping it up, just add or remove its own dismiss button from its own navigation bar. This fits in with encapsulation, but feels very weird from a Cocoa design stand point. For one, when should ViewControllerB assign itself as its presentationController.delegate?

Slight tangent… The default modalPresentationStyle is .FullScreen. If you ask a view controller for its presentationController before you set its modalPresentationStyle you will get a full screen presentation controller. If you then set the modalPresentationStyle to something other than .FullScreen and ask for the presentationController again, you’ll get back the same full screen presentation controller. Still need to file this one, but I’ve seen other presentation controller weirdness so I bet this gets closed “Works as intended.”

So, back to ViewControllerB grabbing its presentationController.delegate… It needs to wait until its modalPresentationStyle has been set. So it could override the setter (or add a property observer in Swift) and do that then, but what if someone else grabs it later. It is a common design pattern, during the whole presentation dance, that the view controller doing the presenting will set itself as the delegate of the view controller to be presented if it wants to be. (Think about presenting a UIImagePickerController.) So it might also make sense that it should do the same with the presentationController.delegate of the view controller to be presented. Now I would need some way to guarantee that anytime ViewControllerB is presented, that it is its own presentationController.delegate.

One of the most important things I’ve learned over the years about Cocoa/CocoaTouch development… If it feels like you’re fighting the frameworks, you’re probably doing it wrong.

ViewControllerA is setting ViewControllerB up for presentation. There is currently no API for a presentedViewController to know how it is currently being presented. Now that adaptability has been introduced, you could set .Popover but actually be in a .FullScreen presentation, and the presentedViewController has no clear way to find that out. There is, however, API for a presentationController to notify its delegate about adaptability. And we already have a design pattern that sets president for ViewControllerA to become the delegate of ViewControllerB.presentationController.

So… “Button, button, who’s got the button?”

My answer, ViewControllerA logically owns the presentation and it should subscribe as the presentationController.delegate. If it’s presenting a content view controller, it should use presentationController(_,viewControllerForAdaptivePresentationStyle:) to wrap that content view controller in a UINavigationController when appropriate and it should push some logic down to any custom containers letting them know they are now being adapted. Yes, that still means that ViewControllerA needs to know way too much about ViewControllerB but really, it’s in the perfect position to do so.