Intercepting and redefining modal presentation from a third-party framework

Ilya Bilov
Bumble Tech
Published in
4 min readDec 15, 2017

The Problem

Some time ago, here at Badoo, we introduced a third-party library to cover our project needs. It was working well, except for one detail: it was presenting UI using iOS modal presentation (typical full screen view controller which is shown from the bottom of the screen), but according to our requirements we needed to embed it in our custom view: Encounters card view, so that it looks like this advertisement built into the card:

An experienced iOS developer might say: “Well, so what’s the big deal here? Just replace modal presentation:

self.present(controller, animated: true, completion: completion)

with child view controller presentation:

self.addChildViewController(controller)
pageView.addSubview(controller.view)
controller.didMove(toParentViewController: self)

And yes, this might help, if we had access to this code, but the problem was, that the presented and view controller was hidden from us (as well as the code invoking the presentation). The third-party library provides us with an API which looks something like this:

public class SomeObject {
public func presentUI(from viewController: UIViewController)
public func dismissUI()
}

And that’s it. No access to internally created view controller, no possibility to change its presentation style.

Internally presentUI() method is calling

viewController.present(self.controller, animated: true, completion: completion)

Now it’s time to grab some tea, and think what we can do (apart from telling our PM “sorry, this is impossible”)

Ideas

Our first thought was to override

func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Swift.Void)? = nil)

the method of presenting view controller. “If everything starts here, why not to try override this method and adjust the presenting behaviour of the viewControllerToPresent object?”
So, let’s do it:

public override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Swift.Void)? = nil) {
self.addChildViewController(viewControllerToPresent)
viewControllerToPresent.view.frame = self.containerView.frame
self.containerView.addSubview(viewControllerToPresent.view)
viewControllerToPresent.didMove(toParentViewController: self)
completion?()
}

This solution somehow worked in the demo application, but it didn’t work at all in Badoo due to the complexity of the child view controller (it has complex hierarchy including WebViews and other heavy components).

After spending some time trying to make it work (we also tried some nasty hacks including UIViewController private API usage and working directly with UIWindow hierarchy), we were about to give up, but luckily one of my colleagues proposed that we should try a little-known iOS API.

The Solution

The most important part of finding the solution was to closely look at all available APIs and try to estimate whether any of them could help us accomplish the task. Once we identified a couple of candidates among the available presentation APIs we tried a couple of them on practice and eventually UIPresentationController turned out to be the exact match to what we were looking for.
Here is what we did:

First, we still need to override

func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Swift.Void)? = nil)

because this is the entry point in our presenting view controller.

Inside, we need to configure the passed modal view controller, in order to specify that it should use custom modal presentation:

public override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Swift.Void)? = nil) {
viewControllerToPresent.modalPresentationStyle = .custom
viewControllerToPresent.modalTransitionStyle = .crossDissolve
viewControllerToPresent.transitioningDelegate = self
super.present(viewControllerToPresent, animated: flag,
completion: completion)
}

Line viewControllerToPresent.modalTransitionStyle = .crossDissolve means that our child view controller will use “cross dissolve” appear animation. If you want to know how to customise the view controllers transition animation, you can read following article: https://www.raywenderlich.com/170144/custom-uiviewcontroller-transitions-getting-started

Another important bit is:
viewControllerToPresent.modalPresentationStyle = .custom
This will allow us to provide a custom presentation controller by implementing UIViewControllerTransitioningDelegate from which we need to override only one method from this protocol in order to modify the presentation style:

     extension MyPresentingViewController: UIViewControllerTransitioningDelegate {
public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
let presentationController = CustomUIPresentationController(presentedViewController: presented, presenting: presenting)
presentationController.customContainer = self.containerView
return presentationController
}
}

In the code above, we just need to provide our own implementation of UIPresentationController as well as provide a custom view for hosting a view of the presented view controller.

Now the final bit: custom UIPresentationController subclass:

class CustomUIPresentationController: UIPresentationController {
public var customContainer: UIView!
public override var frameOfPresentedViewInContainerView: CGRect {
return self.customContainer.bounds
}

public override func presentationTransitionWillBegin() {
self.customContainer.addSubview(self.containerView!)
self.containerView?.frame = self.customContainer.bounds
}
}

We just want to specify destination frame for the presented view controller, and add containerView to our customContainer.

Both presentations use self.present(animated:completion).

Conclusion

Sometimes Apple provides all the necessary tools and mechanisms to achieve a certain goal. If you don’t know how to implement something — don’t try to hack the system straight away! It is always better to spend some time looking around because there might be a native solution available which will save you hours of pain struggling with a hack later on.

Demo

--

--