Adding iOS 7 UI patterns to your app

Today we’ll study how to add controls with blurred background to an iOS app. This UI pattern is very common for iOS 7 apps. Some of the standard UIKit controls (like UINavigationBar or UITabBar) have similar functionality. But, what if you want to add blur to your custom control.

What you need to know is – nothing comes for free. You’ll have to do blurring, and maintaining this blur together with background changes. This example will cover only very simple scenario. But you’ll be able to extend it for more complicated cases.

Here is what we’re going to achieve.
Blurry background
Here we have an element which has a tint color and blurred background. Looks similar to iOS 7’s Notification Center and Control Center.

We won’t be doing too much math for blurring and manipulating images – there is Apple’s sample code called UIImageEffects, which is available for download on Apple Developer Portal (in WWDC2013 Sample Code section).

How will our custom control work:

  1. Rasterise background view (this could be almost any view)
  2. Crop image rectangle which is needed for our floating view
  3. Blur and tint cropped image
  4. Set this processed image as a background for our floating view

And we should not forget to repeat it when background view behind our floating view changes. Our example will cover only one change – interface orientation change.

So, let’s start with view rasterisation. There are several methods available. We’ll use CALayer‘s -renderInContext: method.

UIView *someView = ...;
CGFloat scale = self.view.window.screen.scale;

UIGraphicsBeginImageContextWithOptions(someView.layer.bounds.size, YES, scale);
[someView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

We start new Core Graphics image context. We’re using size of our view, set it to be opaque and provide scale (we want the view to be rendered correctly on Retina screens).
Then we force view’s layer to render itself in this context. And we get UIImage from context. Simple. Nothing really special here.

Now we need to get rectangle that will be background of our view.

CGRect cropFrame = CGRectMake(floatingView.frame.origin.x * scale,
                              floatingView.frame.origin.y * scale,
                              floatingView.frame.size.width * scale,
                              floatingView.frame.size.height * scale);
        
CGImageRef cgCroped = CGImageCreateWithImageInRect(image.CGImage, cropFrame);
UIImage *cropped = [UIImage imageWithCGImage:cgCroped
                                       scale:scale
                                 orientation:UIImageOrientationUp];
CFRelease(cgCropped);

With respect to content scale factor, we’re creating cropping rectangle. In our case floating view is exactly above background view, we don’t need to translate coordinates, so frame of floating view will be cropping rectangle.
Then we use CGImageCreateWithImageInRect function to create CGImageRef for that rectangle from original image. And using CGImageRef we create UIImage.

Now we need to apply blur and tint. This is done using Apple’s example. Simple category methods create image we need.

UIImage *blurred = [cropped applyTintEffectWithColor:[UIColor greenColor]];

Now we can set this image as a background for our floating view.

And when background view changes, we need to repeat these steps to update floating view.

Let’s see how we could respond to interface rotation changes.

-(void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
                                        duration:(NSTimeInterval)duration
{
    UIImageView *oldCroppedImage = self.croppedImage;
    UIImageView *replacingCroppedImage = [[UIImageView alloc] initWithFrame:self.containingView.bounds];
    replacingCroppedImage.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    replacingCroppedImage.translatesAutoresizingMaskIntoConstraints = YES;
    replacingCroppedImage.alpha = 0.0f;
    [self.containingView insertSubview:replacingCroppedImage 
                          aboveSubview:oldCroppedImage];
    self.croppedImage = replacingCroppedImage;
    [self updateBlurredImageForInterfaceOrientation:toInterfaceOrientation];
    [UIView animateWithDuration:duration
                     animations:^{
                         replacingCroppedImage.alpha = 1.0f;
                     } completion:^(BOOL finished) {
                         [oldCroppedImage removeFromSuperview];
                     }];
}

I use this approach:

  1. Floating view is contained in UIView (accessible via self.containingView outlet)
  2. There is an UIImageView which is used as background (accessible via self.croppedImage outlet)
  3. When interface orientation changes, I add new UIImageView just above old one
  4. Initially new UIImageView has alpha of zero and blurred image for new interface orientation
  5. -updateBlurredImageForInterfaceOrientation: updates self.croppedImage with proper image for specific interface orientation
  6. New UIImageView fades in with the duration of interface orientation change duration
  7. Then, old UIImageView is removed from view hierarchy
  8. And we keep self.croppedImage outlet pointing to new UIImageView

This helps to make transition as smooth as possible.

Improvement ideas

As you might see, each time we rotate the device, controller performs expensive rasterisation and blurring operations. It is better to store the result images in some kind of a cache (NSCache, for example). Sample code uses this approach.

Another approach would be to rasterise and blur the whole background view. And then crop only rectangles you need. Moreover, this approach will be required, if background view has a scrollable content – when background content scrolls, you’ll just scroll background image on floating view accordingly.
But, if your background view changes, you’ll have to rasterise it again.

Overall, there are no universal solutions. You’ll have to design the UI the way it will work efficient and look nice. If you design your application so your background view notifies the floating view when it changes you’ll be able to update those simultaneously without significant performance hit. For users, floating view will be just semitransparent, they’ll not notice the hard work behind the scenes.

This example will not help you much, if background view is a movie, or camera output. There are other approaches on how to get those types of views blurred in real-time.

You can find source code for this tutorial on GitHub. Use freely under terms of MIT license.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s