BuySellAds.com

My book Barcodes with iOS is nearing completion. Buy it now to get early access!
Our DNA is written in Objective-C
Jump

Blurring Views on Mac

“Frosted Glass” abounds on iOS 7 and this new look is the new “Corinthian Leather”. Apple has often used design ideas from their mobile OS and let them inform UI design on OS X. This begs the question: where is frosted glass on Mac?

Mac developer Raffael Hannemann offered to do a guest tutorial for Cocoanetics.com demonstrating how to achieve the same view blurring effect on Mac, where you are much less constrained by the GPU performance. On Mac the necessary ingredients for view blurring are readily available.

On iOS Apple kept the necessary APIs for blurring private for the time being because of a severe performance problem that goes hand in hand with live Gaussian blurring. Raffel’s blog post after the break.

iOS 7 has finally landed and is already being used by the majority of the users, according to some early reports. The new design paradigms of “engaging user interfaces”, depth and layers are polarizing and led to the well-known ongoing discussion among the creatives.

One of the most prominent changes in the UI is the frosted glass effect used throughout the system: Central states of the iOS, e.g., the Lock Screen, the Notification Center or the Control Center, are now backed by a heavily blurred background view, that let the user’s content shine through without being distracting.

iOS 7 Control Center with blurred background

App developers, in the rush to modernize their user interfaces, are finding various solutions to re-create the effect on their own or using Apple’s help. And while they are re-inventing the style of mobile applications, the desktop apps for OS X find themselves in a stagnated state — which doesn’t necessarily have to be a bad thing. But one might claim that the UI of upcoming versions of OS X will undergo a similar substantial revision.

Today we will take a quick look at how to create an iOS 7 inspired User Interface for Mac applications. Turns out it isn’t that hard. The result of our guide, a NSView with a blurred background, is quite experimental, but definitely worth checking out. Let’s get started!

Investigation Time

If we take another look at the blur effect on iOS 7, we will notice that there are slightly different effect variations. Some views just seem to blur the background, while others appear downright illuminated, with vibrant colors shining through. That’s actually the case, as mentioned in some of the WWDC 2013 sessions. As an UI/UX designer it is your choice how to craft it. It is just a matter of configuration.

For example to get the vibrant look of the Control Center, the following steps may be sufficient to mimic the effect:

  1. Increase the saturation of the background slightly.
  2. Blur the whole area with a certain, but high radius.
  3. Tint the view with a transparent white color.

We have three parameters here affecting the processing: the saturation factor, the blur radius and the tint alpha.

Core Image Filters

Apple provides great technology both on iOS and on OS X for developers to modify and process images very quickly. The holy grail of blurring backgrounds are Core Image Filters, in short: CIFilter. These filters take one or more images as input, process them using a set of parameters and then produce an output image as a result. One of the pre-defined filters Apple is offering in their SDKs is the CIGaussianBlur filter: “Spreads source pixels by an amount specified by a Gaussian distribution.”, Core Image Filter Reference (Quite helpful bookmark, by the way). As noted in the reference, this filter only takes “inputRadius” as a parameter (the default is 10.0) and that’s all. How to use it in our case?

If we would develop an iOS solution, we should now take a look at Apple’s [NSImage applyLightEffect]; method, which they made accessible publicly. The approach here is to actually render a view hierarchy into a UIGraphicsContext and run the mentioned filter wrapper method. Several github repos follow similar approaches and seem to be quite satisfying. There are even some OS X compatible solutions out there, including CFIFrostedOverlayView, however, their realtime performance didn’t convince us in our first tests.

As it turns out, it doesn’t have to be that hard. Core Animation Layers, CALayers, already provide a nice API to manipulate both their content, their background and even their compositing behavior using the mentioned CIFilters. You can even easily set up a basic layer configuration to get a blur effect right in Interface Builder using the View Effects inspector of Xcode. But we want to build a drop-in, ready-to-use NSView subclass with a clean and dedicated API to set it up.

Introducing RMBlurredView

Initially make sure you have added QuartzCore.framework to the linked libraries of your project. QuartzCore is required to use CoreAnimation layers and filters. To start coding, create a subclass of NSView. The interface for our new class should provide methods to modify the three mentioned filter parameters. Let’s have a look at it:

#import <Cocoa/Cocoa.h>
 
// Make sure to import QuartzCore and add it to your Linked Libraries of your target
#import <QuartzCore/QuartzCore.h>
 
@interface RMBlurredView : NSView 
{
   // Keep a reference to the filters for later modification
   CIFilter *_blurFilter, *_saturationFilter;
}
 
/** The layer will be tinted using the tint color. By default it is a 70% White Color */
@property (strong,nonatomic) NSColor *tintColor;
 
/** To get more vibrant colors, a filter to increase the saturation of the colors can be applied. 
    The default value is 2.5. */
@property (assign,nonatomic) float saturationFactor;
 
/** The blur radius defines the strength of the Gaussian Blur filter. The default value is 20.0. */
@property (assign,nonatomic) float blurRadius;
 
@end

That’s all we need. Next is the actual implementation. Usually when sub-classing NSView, we would now overwrite the [NSView drawRect:] method to customize its appearance. In this case we don’t have to. Instead, we only need to set up the filters.

- (void)setUp 
{
   // Instantiate a new CALayer and set it as the NSView's layer (layer-hosting)
   CALayer *blurLayer = [CALayer layer];
   [self setWantsLayer:YES];
   [self setLayer:blurLayer];
 
   // Set up the default parameters
   _blurRadius = kRMBlurredViewDefaultBlurRadius;
   _saturationFactor = kRMBlurredViewDefaultSaturationFactor;
   [self setTintColor:kRMBlurredViewDefaultTintColor];
 
   // It's important to set the layer to mask to its bounds, otherwise the whole parent view 
   /// might get blurred
   [self.layer setMasksToBounds:YES];
 
   // To apply CIFilters on OS X 10.9, we need to set the property accordingly:
   if ([self respondsToSelector:@selector(setLayerUsesCoreImageFilters:)]) 
   {
      [self setLayerUsesCoreImageFilters:YES];
   }
 
   // Set the layer to redraw itself once it's size is changed
   [self.layer setNeedsDisplayOnBoundsChange:YES];
 
   // Initially create the filter instances
   [self resetFilters];
}

The most important detail however is the [RMBlurredView resetFilters], though, which instantiates all required objects concerning the effect:

- (void)resetFilters 
{
   // To get a higher color saturation, we create a ColorControls filter
   _saturationFilter = [CIFilter filterWithName:@"CIColorControls"];
   [_saturationFilter setDefaults];
   [_saturationFilter setValue:[NSNumber numberWithFloat:_saturationFactor] 
                        forKey:@"inputSaturation"];
 
   // Next, we create the blur filter
   _blurFilter = [CIFilter filterWithName:@"CIGaussianBlur"];
   [_blurFilter setDefaults];
   [_blurFilter setValue:[NSNumber numberWithFloat:_blurRadius] forKey:@"inputRadius"];
 
   // Now we apply the two filters as the layer's background filters
   [self.layer setBackgroundFilters:@[_saturationFilter,_blurFilter]];
 
   // ... and trigger a refresh
   [self.layer setNeedsDisplay];
}

It is almost self-explanatory. First we set up the NSView to be layer-backed, otherwise we could not apply any CIFilter. We mask the view to its bounds (otherwise the whole parent view will be blurred). We set its tint color and then (re)build the filters. The filters will be rebuilt whenever one of the parameters changed, which we make sure by providing custom setter methods:

- (void) setTintColor:(NSColor *)tintColor 
{
   _tintColor = tintColor;
 
   // Since we need a CGColor reference, store it for the drawing of the layer.
   if (_tintColor) 
   {
      [self.layer setBackgroundColor:_tintColor.CGColor];
   }
 
   // Trigger a re-drawing of the layer
   [self.layer setNeedsDisplay];
}
 
- (void)setBlurRadius:(float)blurRadius 
{
   // Setting the blur radius requires a resetting of the filters
   _blurRadius = blurRadius;
   [self resetFilters];
}
 
- (void) setSaturationFactor:(float)saturationFactor 
{
   // Setting the saturation factor also requires a resetting of the filters
   _saturationFactor = saturationFactor;
   [self resetFilters];
}

Now it is important to not forget to actually call [RMBlurredView setUp]. We will do this in both the initWithFrame:; and the initWithCoder:; methods. This will ensure that the method will be called whenever the NSView subclass is being instantiated by the NIB loader or manually by creating an instance programmatically.

- (id)initWithFrame:(NSRect)frame
{
   self = [super initWithFrame:frame];
 
   if (self) 
   {
      [self setUp];
   }
 
   return self;
}
 
- (id)initWithCoder:(NSCoder *)coder
{
   self = [super initWithCoder:coder];
 
   if (self) 
   {
      [self setUp];
   }
 
   return self;
}

That’s it, were done! That was easy. Compare this to the approach of manually fetching an image representation of the views underneath it. Let’s thank CoreImage!

RMBlurredView in the demo app
One important aspect we have to mention: The blurring won’t work if the parent view is not layer-backed. Make sure you call setWantsLayer:YES; or check the corresponding flag in the View effects inspector in Xcode directly. This also leads us to the pitfalls we need to address.

Pitfalls

As mentioned, it is important that the parent view, that is the view visually underneath the RMBlurredView, is layer-backed. This might be no problem for simple instances, however when used in conjunction with some more advanced NSView subclasses, including WebView, IKImageBrowserView and others, unexpected behavior might occur — be careful!

In terms of performance please note that the larger the area of the view is, the more CPU time must be invested to process the filters. Also keep in mind the doubled pixel density of Retina Displays, which require four times the size for the internal textures used to render the Gaussian filter. The textures dimensions also may be constrained on some systems, so that blurring large areas might be not the best idea. Thanks to @indragie for the hint!

We also experienced some problems when we set up a view hierarchy in our XIB using the Interface Builder. Sometimes the blurring effect doesn’t work at all or the layer order is wrong. Setting up the hierarchy programmatically fixed this, though. But hey, it’s experimental. Leave a comment if you have any idea!

Advanced NSScrollViews

One particular annoying problem we encountered during testing is the following: Think of the new Messages app on iOS 7. You can see your messages shining through the app’s titlebar. However the actual scroll area of the UIScrollView is smaller than its overall visible area. Therefore you would have to add a padding to the content view of the scroll view.

NSScrollView with reduced scrollable area

While it is easy to implement this on iOS, it turns out to be tricky on OS X. It is possible to move the vertical scrollbar more into the inner of the view by simply re-defining the frame of the NSScroller, but once we set the NSScrollView (or a WebView, which contains NSScrollViews for each WebFrame) to be layer-backed, the redefined frame will be ignored.

This is critical for our usage scenarios. We want scrollable content to shine through our RMBlurredView rects with correctly positioned scrollbars. Currently our only work-around for this consists of RMSecondaryScroller, an NSScroller sub-class which you can use to get a secondary scrollbar for your NSScrollView, and which you can position and shrink the way you like to. It works, but it is a kind of hacky way.

Conclusion

You can grab the archive from our github repository. To illustrate the technique, check out the new look of our app Chat Heads, which will get a new look for the conversation view. What do you think?

The RMBlurredView used in the Chat Heads app
Raffael

About the Author

Raffael Hannemann is developer of Chat Heads (released Mac app) and Sinus (currently BETA). He lives and works in Frankfurt, Germany.  You can follow him on GitHub and Twitter.


Categories: Recipes

%d bloggers like this: