<> Posted on January 8, 2015 in Code
For the App I'm working on I wanted to show a small tutorial on first launch and, after that, when the user requested it. I like the blur effect introduced by Apple in iOS 7, so I went ahead and tried to implement it but I had some trouble presenting the tutorial over a UISplitViewController.
Finding how to display a blurred background was no problem thanks to this Stack Overflow thread. I added a function on my tutorial UIViewController to insert the background with the blur effect on the presented UIView which is called on viewDidLoad.
private func setBlurredBackground() {
//only apply the blur if the user hasn't disabled transparency effects
if !UIAccessibilityIsReduceTransparencyEnabled() {
let blurEffect = UIBlurEffect(style: UIBlurEffectStyle.Light)
let blurEffectView = UIVisualEffectView(effect: blurEffect)
blurEffectView.frame = view.bounds //view is self.view in a UIViewController
view.backgroundColor = view.backgroundColor?.colorWithAlphaComponent(0.0)
view.insertSubview(blurEffectView, belowSubview: dismissButton)
// if you have more UIViews on screen, use insertSubview:belowSubview:
// to place it underneath the lowest view
//add auto layout constraints so that the blur fills the screen upon rotating device
blurEffectView.setTranslatesAutoresizingMaskIntoConstraints(false)
view.addConstraint(NSLayoutConstraint(item: blurEffectView,
attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal,
toItem: view, attribute: NSLayoutAttribute.Top, multiplier: 1, constant: 0))
view.addConstraint(NSLayoutConstraint(item: blurEffectView,
attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal,
toItem: view, attribute: NSLayoutAttribute.Bottom, multiplier: 1, constant: 0))
view.addConstraint(NSLayoutConstraint(item: blurEffectView,
attribute: NSLayoutAttribute.Leading, relatedBy: NSLayoutRelation.Equal,
toItem: view, attribute: NSLayoutAttribute.Leading, multiplier: 1, constant: 0))
view.addConstraint(NSLayoutConstraint(item: blurEffectView,
attribute: NSLayoutAttribute.Trailing, relatedBy: NSLayoutRelation.Equal,
toItem: view, attribute: NSLayoutAttribute.Trailing, multiplier: 1, constant: 0))
}
}
Now it's just a matter of presenting the view controller modally with presentViewController:animated:completion:, or so I thought. None of the modalPresentationStyle options allowed the blur effect view to get the context from the presenting view controller, so the blur didn't work at all.
Turns out I had to set the parent controller of the presented view controller, the tutorial view controller, to the rootViewController of the appDelegate's window and add it as a child of the rootViewController. To do this I added this convenience method to the presented view controller that besides setting up the view hierarchy also presents the new view with a fade in animation:
func setParentController(parentController: UIViewController) {
view.alpha = 0.0
view.frame = parentController.view.bounds
parentController.view.addSubview(view)
parentController.addChildViewController(self)
UIView.animateWithDuration(0.5, animations: { self.view.alpha = 1.0})
}
And another one to remove it from the view hierarchy and dismiss it using the completion block of a fade out animation to perform the necessary actions:
private func removeFromParentController() {
UIView.animateWithDuration(0.5, animations: { self.view.alpha = 0 }) { (finished) -> Void in
self.willMoveToParentViewController(nil)
self.view.removeFromSuperview()
self.removeFromParentViewController()
}
}
Then from the presenting view controller I can easily present the tutorial over the full screen using setParentController(rootViewController), and dismiss it from itself when needed with removeFromParentController.
I'm taking advantage of the new UISplitViewController functionality in iOS 8 for the iPhone, and this works perfectly well there as it behaves as a UINavigationController. But when it acts as an actual UISplitViewController, as it's the default behaviour on the iPad, there's some trouble waiting in the corner. Setting our overlay view controller as a child of the root view controller throws this warning message to the log console:
2015-01-05 17:52:53.342 BlurBackgroundViewOverSplitView[3268:528140] Split view controller
<UISplitViewController: 0x7fdd58c4e300> should have its children set before layout!
This is because the root view controller is the split view controller and it doesn't seem to like having another child view controller besides the master and detail view controllers it already has. It should be noted that besides the warning message everything else seems to work as expected, but I don't like having warning messages in my logs so I tried to find a solution.
The fix I found came from watching the video for the talk View Controller Advancements in iOS 8 from WWDC 2014 where they show in a demo App how to force an iPhone to display a UISplitViewController by setting a root view controller that overrides the traits of its child, a UISplitViewController. Using the same idea, I tried injecting a new root view controller as a parent of the UISplitViewController. With this new root view controller everything works fine and the UISplitViewController doesn't complain about its children when adding the view controller with the blurred background because it isn't affected at all, they're both childs of the same parent view controller.
The root view controller is a generic UIViewController that has a viewController property pointing to its only child. This property has a willSet method to remove the current viewController before setting it to a new one, and a didSet method to add the new viewController to the hierarchy and to set some autolayout constraints:
var viewController: UIViewController? {
willSet(newViewController) {
if viewController != newViewController {
if viewController != nil {
self.viewController?.willMoveToParentViewController(nil)
self.viewController?.view.removeFromSuperview()
self.viewController?.removeFromParentViewController()
}
}
}
didSet {
if viewController != nil {
addChildViewController(viewController!)
view.addSubview(viewController!.view)
let newView = viewController!.view
let views = ["newView":newView]
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("|[newView]|",
options: NSLayoutFormatOptions(0), metrics: nil, views: views))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[newView]|",
options: NSLayoutFormatOptions(0), metrics: nil, views: views))
viewController?.didMoveToParentViewController(self)
}
}
}
Finally, from the AppDelegate's application:didFinishLaunchingWithOptions: the new root view controller is created and injected into the hierarchy, setting the split view controller as its child:
func application(application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
let splitViewController = self.window!.rootViewController as UISplitViewController
let navigationController =
splitViewController.viewControllers[splitViewController.viewControllers.count-1]
as UINavigationController
navigationController.topViewController.navigationItem.leftBarButtonItem =
splitViewController.displayModeButtonItem()
splitViewController.delegate = self
// Injects a view controller as root to be able to display a view with a blurred background
// in full screen over split views.
// Also, it may eventually be useful to force traits on the UISplitViewController.
let rootViewController = RTSRootViewController()
window?.rootViewController = rootViewController
rootViewController.viewController = splitViewController
return true
}
I've uploaded a zip file with a very simple XCode project, not worthy of GitHub, demostrating this functionality.