13

The upcoming OSX 10.10 ("Yosemite") offers a new type of view, NSVisualEffectView, which supports through-the-window or within-the-window translucency. I'm mostly interested in through-the-window translucency, so I'm going to focus on that in this question, but it applies to within-the-window translucency as well.

Using through-the-window translucency in 10.10 is trivial. You just place an NSVisualEffectView somewhere in your view hierarchy and set it's blendingMode to NSVisualEffectBlendingModeBehindWindow. That's all it takes.

Under 10.10 you can define NSVisualEffectViews in IB, set their blending mode property, and you're off and running.

However, if you want to be backwards-compatible with earlier OSX versions, you can't do that. If you try to include an NSVisualEffectView in your XIB, you'll crash as soon as you try to load the XIB.

I want a "set it and forget it" solution that will offer translucency when run under 10.10 and simply degrade to an opaque view when run on earlier OS versions.

What I've done so far is to make the view in question a normal NSView in the XIB, and then add code (called by awakeFromNib) that checks for [NSVisualEffectView class] != nil, and when it's the class is defined, I create an instance of the NSVisualEffectView, move all my current view's subviews to the new view, and install it in place. This works, but it's custom code that I have to write every time I want a translucent view.

I'm thinking this might be possible using an NSProxy object. Here's what I'm thinking:

Define a custom subclass of NSView (let's call it MyTranslucentView). In all the init methods (initWithFrame and initWithCoder) I would throw away the newly created object and instead create a subclass of NSProxy that has a private instance variable (myActualView). At init time it would decide to create the myActualView object as an NSVisualEffectView if OS>=10.10, and a normal NSView under OS<10.10.

The proxy would forward ALL messages to it's myActualView.

This would be a fair amount of fussy, low-level code, but I think it should work.

Has anybody done something like this? If so, can you point me in the right direction or give me any pointers?

Apple is MUCH more open with the Beta agreement with Yosemite a than with previous Betas. I don't think I'm violating my Beta NDA by talking about this in general terms, but actual code using NSVisualEffectView would probably need to be shared under NDA...

Duncan C
  • 128,072
  • 22
  • 173
  • 272
  • Another solution could be to use a Category on `NSView`, something like `-(id)initVisualEffectView` which do the effect, returning a `NSView` or a `NSVisualEffectView` since I guess it inherits from `NSView`. Or if you're using nibs, maybe `-(void)applyVisualEffectView` (to self). – Larme Sep 18 '14 at 15:40
  • What would the category method do for nib-based views? I guess it could create a new view that's the full size of the view, move all the contents of the view to that view, and make it the only subview of the view. That could work... – Duncan C Sep 18 '14 at 16:05
  • @Larme, I should have tagged you in my response comment so you get notified... – Duncan C Sep 18 '14 at 16:16
  • I got notified the first time. After some perspective, a custom class could be better. – Larme Sep 19 '14 at 09:24

3 Answers3

12

There is a really simple, but somewhat hacky solution: Just dynamically create a class named NSVisualEffectView when your app starts. Then you can load nibs containing the class, with graceful fallback on OS X 10.9 and earlier.

Here's an extract of my app delegate to illustrate the idea:

AppDelegate.m

#import "AppDelegate.h"
#import <objc/runtime.h>

@implementation PGEApplicationDelegate
-(void)applicationWillFinishLaunching:(NSNotification *)notification {
    if (![NSVisualEffectView class]) {
        Class NSVisualEffectViewClass = objc_allocateClassPair([NSView class], "NSVisualEffectView", 0);
        objc_registerClassPair(NSVisualEffectViewClass);
    }
}
@end

You have to compile this against the OS X 10.10 SDK.

How does it work?

When your app runs on 10.9 and earlier, [NSVisualEffectView class] will be NULL. In that case, the following two lines create a subclass of NSView with no methods and no ivars, with the name NSVisualEffectView.

So when AppKit now unarchives a NSVisualEffectView from a nib file, it will use your newly created class. That subclass will behave identically to an NSView.

But why doesn't everything go up in flames?

When the view is unarchived from the nib file, it uses NSKeyedArchiver. The nice thing about it is that it simply ignores additional keys that correspond to properties / ivars of NSVisualEffectView.

Anything else I need to be careful about?

  1. Before you access any properties of NSVisualEffectView in code (eg material), make sure that the class responds to the selector ([view respondsToSelector:@selector(setMaterial:)])
  2. [[NSVisualEffectView alloc] initWithFrame:] still wont work because the class name is resolved at compile time. Either use [[NSClassFromString(@"NSVisualEffectView") alloc] initWithFrame:], or just allocate an NSView if [NSVisualEffectView class] is NULL.
Jakob Egger
  • 11,981
  • 4
  • 38
  • 48
  • Cool. I like that approach. (voted) What I ended up doing in my client's project was to create a custom subclass of UIView. I use that instead of a visual effects view. At runtime, the view checks to see if NSVisualEffectView is defined. If so, it creates one, with it's bounds as the frame, and moves over all of its subviews into this new child view, then installs the NSVisualEffectView as it's (only) subview. I like your approach much better though, as my approach changes the view hierarchy at runtime, adding an extra subview. – Duncan C Nov 04 '14 at 17:06
  • 3
    Your solution is pretty smart, but it has one more drawback, that was not yet mentioned. When you setup an NSVisualEffectView in a XIB, that has "Builds for" option (located in the first tab of the inspector pane) set to the deployment target lower than 10.10 you will get a build error that states "NSVisualEffectView unavailable prior to OS X 10.10". – Konstantin Pavlikhin Feb 12 '15 at 16:29
3

I just use this category on my top-level view.

If NSVisualEffects view is available, then it inserts a vibrancy view at the back and everything just works.

The only thing to watch out for is that you have an extra subview, so if you're changing views around later, you'll have to take that into account.

@implementation NSView (HS)

-(instancetype)insertVibrancyViewBlendingMode:(NSVisualEffectBlendingMode)mode
{
    Class vibrantClass=NSClassFromString(@"NSVisualEffectView");
    if (vibrantClass)
    {
        NSVisualEffectView *vibrant=[[vibrantClass alloc] initWithFrame:self.bounds];
        [vibrant setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
        [vibrant setBlendingMode:mode];
        [self addSubview:vibrant positioned:NSWindowBelow relativeTo:nil];

        return vibrant;
    }

    return nil;
}

@end
Confused Vorlon
  • 9,659
  • 3
  • 46
  • 49
  • 2
    I'm not sure this is a good idea, because then all the buttons and labels end up *above* the `NSVisualEffectView`, instead of *inside*. This results in some subtle display issues. – Jakob Egger Oct 13 '14 at 08:34
  • can you elaborate (or post a screenshot?). My expectation is that the buttons won't be translucent, and that labels will (which is what I see here). I don't have a clear idea of what is 'correct' though... – Confused Vorlon Oct 13 '14 at 11:38
  • For example, text fields adjust label color according to the visual effect view appearance. Or template images are rendered vibrant. But the view needs to be a subview of NSVisualEffectView. More details in WWDC 2014 session 220 "Adopting Advanced Features of the New UI of OS X Yosemite" (Transcript: http://asciiwwdc.com/2014/sessions/220) – Jakob Egger Oct 16 '14 at 10:21
  • The code snippet in the Category is useful regardless of the above comments. Thanks. – lindon fox Jan 03 '15 at 12:09
0

I wound up with a variation of @Confused Vorlon's, but moving the child views to the visual effect view, like so:

@implementation NSView (Vibrancy)


- (instancetype) insertVibrancyView
{
    Class vibrantClass = NSClassFromString( @"NSVisualEffectView" );
    if( vibrantClass ) {
        NSVisualEffectView* vibrant = [[vibrantClass alloc] initWithFrame:self.bounds];
        [vibrant setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];

        NSArray* mySubviews = [self.subviews copy];
        for( NSView* aView in mySubviews ) {
            [aView removeFromSuperview];
            [vibrant addSubview:aView];
        }

        [self addSubview:vibrant];

        return vibrant;
    }

    return nil;
}

@end
zpasternack
  • 17,838
  • 2
  • 63
  • 81