Step deeper into Dark Mode adoption in your macOS application. Learn about the powerful Cocoa technologies at the core of Dark Mode, and take a detailed look at the APIs and best practices for adapting to this beautiful new look.
I'm Matt Jacobson. I'll be joined later on stage by my colleague, Jeff Nadeau.
We're engineers in the Cocoa Frameworks group at Apple.
We are super excited to talk to you today about the awesome Dark Mode in Mojave.
Now, in the intro session yesterday, you learned all the things you need to get started adapting your app for Dark Mode, like rebuilding on the macOS 10.14 SDK. Making use of dynamic colors instead of static or hardcoded colors.
Making correct use of template images and materials.
And, most of all, making use of the new features in Xcode 10 to define custom color and image assets specifically for Dark Mode.
Now, if you need a review on any of those topics, I highly recommend going back and watching the intro session on video later.
Now, most UI will look great in Dark Mode using just those techniques.
In fact, some of our system apps required no other changes. It was great.
But we know some cases will require a little bit more work and that's what we're going to get into in this session.
We're going to cover six main areas today.
First, the appearance system, how it works, and how you can make use of it in your custom views.
Second, materials, what they are, and how you can best make use of them in your UI. Then I'll hand it over to Jeff and he'll talk about vibrant blending, which is an awesome way to make your views look great.
As well as reacting correctly to selection using something called background style.
Finally, he'll wrap it up with some discussion on how to back deploy your app to older versions of macOS while still supporting Dark Mode, as well as some general tips and tricks for polishing your apps for Dark Mode.
All right. Let's get started. So, in Mojave, your app will need to look great in light and dark.
And the way you'll do that is using something called NSAppearance.
NSAppearance is the theme system used throughout Cocoa and the key part about it is you only have to maintain a single view hierarchy and NSAppearance will help it look great in light and dark.
Now, in addition to being at the core of Dark Mode, we've already been using NSAppearance for several years and it's underlied such features as the high contrast mode of macOS as well as the touch bar UI designed specifically for that awesome piece of hardware.
Now, previously we've had one main appearance, one main light appearance for aqua windows and we called aqua.
And, of course, in 10.14, we're introducing a second appearance for aqua windows for Dark Mode called darkAqua.
These objects contain all the assets that views draw with.
So, any time you use system dynamic colors or standard effects or named images or even just standard Cocoa controls, this is where all that stuff is coming from.
And AppKit will automatically provide appearances for all of you views and windows based on the user's light/dark preference in system preferences once you link on the macOS 10.14 SDK.
So, here's our beautiful Chameleon Wrangler app that Rachel and Taylor created in the intro session and you can see once we linked it on the macOS 10.14 SDK, AppKit went ahead and automatically gave it the darkAqua appearance.
Now, that's great, but what if we want to change the appearance? For example, what if we wanted to change the appearance of this notes view? We might think that in the dark appearance we still might want the notes view to appear light.
Well, you can do that using something called NSAppearanceCustomization.
Now, this is a protocol, but it's not a protocol you have to go off and adopt in your applications.
It's already adopted by NSView and NSWindow and, in Mojave, NSApplication conforms as well.
It's a pretty simple protocol. It just adds two properties.
First property is appearance and this is where you can override the appearance for a particular object.
Now, it's an optional NSAppearance because, if you set it to nil, the object will simply inherit its appearance from its ancestors.
There's also effective appearance and this is a read-only property that you can use to find out what appearance a view will draw with.
And of course, to use this, you'll have to get the right NSAppearance object and you can do that pretty easily using the NSAppearance named initializer.
Just pass aqua or darkAqua based on which appearance you want and then you can go ahead and just assign that to the appearance property of the object that you'd like to customize. So, in this case, we'll assign the aqua appearance to the appearance property of the text view and now it uses the light appearance. All right.
That was pretty easy, so let's take a look at another case.
You might have a window that kind of hangs off of a particular view.
And you probably want its appearance to match the view it hangs off of.
Now, we could just assign the aqua appearance to this window just like we did to the view, but what we really want is something a little stronger. We want its appearance to inherit from the view and we can do that-- first of all, AppKit will automatically do this for us for a number of common windows, like menus, popovers, tool tips, and sheets, so you don't have to worry about it in those cases.
But, for custom cases like this, there's new API in Mojave that you can use to do this.
It's called Appearance Source.
Now, this is a property that takes any object that conforms to that NSAppearanceCustomization protocol-- so, views and windows-- and you just assign it to the appearanceSource property and the window will inherit its appearance from that object.
So, in this case, we'll assign the text view to the appearanceSource property of that child window and now it's appearance will always inherit from that view no matter what it is.
In fact, you should think of the appearance system as a sort of hierarchy.
Similar to the view hierarchy you're probably familiar with, but extending to windows and the application as well.
And when we ask AppKit for a view's effective appearance, AppKit will simply walk up this hierarchy until it finds an object with a specified appearance and that's the appearance we'll use.
So, now that we know how objects get an appearance and how the appearance system works, let's talk about how you can use it in your custom views and controls.
Here's an example. Let's say I wanted this custom header view here to use a different color in light and dark appearance.
Now, we already know in Xcode 10 I can go into the asset catalog editor and specify specific color assets for light and dark.
But then how do I use that in my custom view? Well, here's one way that seems tempting but won't work and I'll show you why.
First, we'll add an NSColor property to our view.
And in init, we'll use that color to populate our layer.
And if the color changes, we'll go ahead and update our layer there too.
Let's try that out.
OK. It looks pretty good in light, but if we switch to dark, we can see our color didn't actually change.
And that's because even though our NSColor is dynamic, the CG color that we get from it is static. It won't change for the appearance.
And, since we configured our layer in our initializer, we didn't get a chance to run any code when the appearance changed.
Now, the key takeaway from this is you need to do your appearance sensitive work in specific areas.
Specifically, the update constraints, layout, draw, and update layer methods of NSView.
Now, AppKit will automatically call these methods as needed when the appearance changes.
And if you need to trigger them manually, of course you can always use the needsUpdateConstraints, needsLayout, and needsDisplayProperties and AppKit will automatically call them.
So, let's go back to our example.
Instead of overriding init, we'll implement updateLayer and there we can go ahead and safely populate our layer by asking our NSColor for a CG color.
And if our color changes, instead of updating our layer right there, we'll just set the needsDisplay property to true.
AppKit will come back around automatically and call updateLayer.
So, let's run it again.
Still looks good in light. And now it uses the correct color in dark just like we wanted, so that's great.
Now, what if we want to do something a little more complicated that might not be expressible just with dynamic colors or images? For example, maybe I would like to add this nice white glow behind Chloe's beautiful face here, but only in Dark Mode.
How would I do that? Well, for cases like that, we have new API in Mojave that you can use to match against your view's appearance. Let me show you how it works.
So, in this view, I'll override the layout method and I'll switch on effectiveAppearance bestMatch(from:, I'll pass an array with all of the appearance names that my view happens to know about. In this case, aqua and darkAqua.
Then it's just a matter of implementing behavior for each of those appearances.
So, for the aqua appearance, I'll simply use my imageView with Chloe's face as a subview.
And for darkAqua, I'll not only use that imageView, but I'll also through my glowView behind it.
Finally, I'll implement a default case and this is for appearances my view doesn't know about and that includes potential appearances Apple might come out with in the future.
Let's take a look at what it looks like.
So, there it is in light, no glow, that's what we wanted.
Switch to dark and we have that glow.
Let's talk for a minute about high contrast.
So, I said before that we've been using NSAppearance for the high contrast mode of macOS.
And one of the nice side effects of doing all this work to support Dark Mode is it makes it really easy to support high contrast really well as well. As a reminder, high contrast is enabled through the increase contrast checkbox in system preferences.
And in this mode, colors are changed so that control bounds and other kinds of boundaries are more easy to see.
Now, in this mode, AppKit automatically replaces the aqua and darkAqua appearances with high contrast counterparts.
Now, these high contrast appearances inherit from their normal contrast versions.
So, what that means is any code you've written to take advantage of Dark Mode will automatically apply in high contrast Dark Mode.
But you can go even further.
In Xcode 10, if you check this high contrast checkbox in the asset catalog editor, it'll allow you to specify color and image assets specifically for the high contrast versions of the appearances.
Now, you can also use those appearance names in code. You might be temped to think, well, great, I'll just pass them to NSAppearance themed and I'll get the NSAppearance object and I'll do something with that, but that won't work.
Those appearances are only available through system preferences.
But what you can do is pass them to bestMatch(from:) just like we did before for Dark Mode to implement custom programmatic behavior.
OK. Let's talk for a minute about sublayers.
I know a lot of you out there have views that manage their own sublayers and there are important things to be aware of for Dark Mode.
Primarily, you need to know that custom sublayers will not inherit your view's appearance automatically.
Now, the easiest fix for this is to switch them from being sublayers to subviews. If you do that, AppKit will automatically handle the appearance inheritance for those views, just like any other view.
Otherwise, you'll have to manage those layers manually using a couple techniques that I'll talk about now, viewDidChange EffectiveAppearance and the concept of the current appearance.
So, first viewDidChange EffectiveAppearance.
This is a new method on NSView that you can override to find out when your view's effective appearance changes.
Now, this is a good time to perform any custom invalidation you might need to do or drop any caches that are no longer relevant.
But remember you don't need to invalidate the view itself here, AppKit will do that for you automatically.
Second, the concept of the current appearance.
Now, this is a thread local variable that you can access through a class property on NSAppearance.
If you're familiar with concepts like the current NSGraphics context or the current NSProgress, you already know what I'm talking about.
If not, just remember that this is the appearance used to resolve dynamic colors and images.
AppKit will set up the current appearance automatically for you before we call any of those special NSView methods we talked about before, like updateConstraints, layout, draw, and updateLayer, but you can also set it up yourself where necessary and let's take a look at an example why you might do that.
So, here's a custom that maintains some sublayers.
I'll override this new viewDidChange EffectiveAppearance method and I'll set my sublayer needsDisplay.
Now, if I didn't do this, my sublayer wouldn't update when my view's effective appearance changed. It would just stay the same. And then in my layer delegate routine, I'll save off the current appearance for later and then I'll go ahead and set the current appearance to my view's effective appearance.
Then I can go ahead and update my layer.
Now, if I hadn't set the current appearance before this, this code wouldn't be using my view's appearance and so it would end up looking wrong.
Finally, when I'm done, I'll just restore the old current appearance.
Here's another thing to be aware of if you're managing layers.
You might have code that looks like one of these two examples.
Either you're setting the contents of a layer to an NSImage or you're using the layer contents for content scale API to create layer contents from an image for your layer.
If you have code like this, you should know that the image will not automatically inherit the appearance.
As before, the best fix is to switch to views. In this case, NSImageView.
NSImageView will take care of this detail as well as a bunch of others automatically, so do that if you can. Otherwise, you'll need to create a CGImage from your NSImage for your layer.
And you'll do that using the cgImage(forProposedRect:, context:, hints: API on NSImage.
And you'll have to be careful to do this at a point where the current appearance is correct. So, a good place to do it is in your updateLayer method.
All right, so that's appearance.
Now let's talk about materials. Now, you've probably heard that materials are one of the building blocks of the modern Mac UI, but you may have wondered to yourself, well, what exactly is a material, so let's start with a definition.
Materials are dynamic backgrounds that make use of effects like blurs, gradient, tinting, translucency, and they provide a sense of depth or context to your UI, as well as just a bit of added beauty.
Here's a pretty typical Mac desktop and you can see all the different places where we're using these material effects-- actually, this isn't even all of them.
Now, AppKit automatically provides materials in a number of common places, like the title bars and backgrounds of windows, table views, sidebars, popovers, menus, and even other places as well. But you can create a material yourself and add it to your UI using a view called NSVisualEffectView.
If you're not familiar with NSVisualEffectView, quite simply, it's a view that shows a material.
And if you want to use one, you'll need to be aware of three main properties that you'll have to set up and I'll go through these in order. The state, blendingMode, and material properties.
So, first, the state property.
This controls whether the material uses the active window look.
Now, by default, the material will just match its containing window and so when that window is active it'll look active.
When the window's inactive, the material will look inactive.
But you can also specify this specifically to be active or inactive if you'd like to control it manually.
Second, the blendingMode property.
This property controls whether the material punches through the back of the window. Let me show you what I mean by that.
Here's a preview using two different materials.
For one, this title bar material, if we peel it back, we can see that it's blending the contents within the window, including that color image there.
So, it's not punching through the back of the window.
There's also the sidebar material and if we peel it back we can see it's blurring the contents behind the window, so it's punching through the back so it can see to the windows behind it as well as the desktop.
So, by default, a visual effect view will be in behind window mode, but you can control that using the blendingMode property.
Finally, the material property.
This property encapsulates the material effect definition.
What do I mean by that? That means the exact recipe of blur, translucency, gradience, tinting-- that all depends on the material property.
Now, when we first started using materials in Yosemite, we had two main materials, the light and dark materials, and those served us really well at the time, but since then we've really expanded our use of materials across the system.
And now with Dark Mode, it no longer really makes sense to specify a material just as light or dark.
Instead, we have something called semantic materials.
Now, if you're familiar with semantic colors, you know that they're named after where they're used, not necessarily what they look like.
Same thing for semantic materials.
The menu material, for example, will always look like system contextual menus, regardless of light versus dark.
And in Mojave, we're introducing a bunch more semantic materials so that you can always use the right one for your specific use case. In fact, these semantic materials are now our preferred way of using materials and we're deprecating these non-semantic materials like light, dark, medium light, and ultra dark.
If you're using one of these materials, now is a great time to go ahead and switch over to a semantic material that's right for your use case.
Just to give you an idea of where we're using these semantic materials across the system, here's the Finder using the title bar and sidebar materials.
Here's Mail using the header view and content background materials.
Here's our Chameleon Wrangler app using the underPageBackground material.
And here's system preferences using the window background material.
Now, of course this window background material, as you've probably heard, is one of these special desktop-tinted materials new in Mojave. And the way these work is they pick up a slight tint from the desktop picture based on the window's location onscreen.
And the idea here is to help your window blend in with the windows on the rest of the system.
Again, the easiest way to get one of these desktop-tinted materials is to use the automatic support in NSWindow, NSScrollView, NSTableView, and NSCollectionView.
The default configurations of these objects will come with this desktop-tinted effect.
You can also configure NSBox to get these materials by setting its type to custom and selecting one of these fill colors.
It'll use the corresponding NSVisualEffectView material.
Here's an example.
I'll set my box's type to custom and then I'll set its fillColor to the underPageBackgroundColor.
Of course, I can also use NSVisualEffectView, I can set it's material property to the underPageBackground material. Now, the advantage of using NSBox is it's back-deployable actually all the way back to Leopard.
VisualEffectView, on the other hand, gives you a little more flexibility and I'll give you an example of that later.
So, just as a reminder, these materials will show their untinted color in light.
And in dark, they'll show that desktop-tinting effect.
But remember that the tint effect can be disabled. Let me show you why.
So, in Mojave, you can choose an accent color for the system.
And if I switch this over to graphite, you'll probably first notice that all the controls lost their colored accents, but those desktop-tinted materials also lost their tint.
So, just make sure you're not depending on that tint being there in any way. Now, VisualEffectView by default will show its material in its frame rectangle like this.
And that's pretty great, but what if I wanted to show a custom UI element with this material, like say a chat bubble.
How would I do that? Well, here's one way that seems tempting, but won't work, and I'll show you why.
We'll first implement the draw method on NSView and then I'll go get my custom chat bubble BezierPath.
And then I'll fill with the controlBackgroundColor in that path. Now, if you do that, you'll find it looks something like this and it looks pretty good, but if we zoom in closely, you'll see that the bubbles are not getting that desktop-tinting effect that we want. It's just a plain gray.
So, what went wrong? Well, this effect is provided by the Quartz window server like a lot of our other material effects.
And what this means is it updates asynchronously from your application and this is great for performance, but it also means that you can't directly draw with that color or get it's RGB values.
Instead, you can use the maskImage property of VisualEffectView to do something very similar.
maskImage is an optional NSImage on VisualEffectView that VisualEffectView will use to mask its material, the material that it shows.
And in addition to using standard art-based images, you can use drawing handler images to simulate drawing with the material. Let me show you an example.
So, I'll go back to my view, I'll override layout, and I'll go ahead and add a VisualEffectView.
I'll set its material to the contentBackground material, and then I'll create a drawing handler image using the NSImage size flipped initializer that takes a block.
In it, I'll set the white color-- this color doesn't really matter as long as its opaque. And then I'll go ahead and fill with my path. Then I'll set that image ask the maskImage on my VisualEffectView.
Let's look at it now. Looks a lot better.
And, if we look side by side, we can really see the difference. So, this technique works with any material, but just remember that only the alpha channel of the image is used for the mask. This is similar to template images.
And the mask only masks the material, not any subviews or other descendent views of the VisualEffectView.
A common technique is to provide a resizable image for this-- for the maskImage using the capInsets and resizingMode properties of NSImage.
And this is really good for performance.
OK. With that, I'll hand it off to Jeff, who is going to talk about vibrant blending.
All right. Thank you, Matt.
So, now that we've had a look at our great materials, I want to cover the things that we draw in front of those materials, particularly the materials that we use that pull in part of the background and provide that really awesome blur effect.
So, if we revisit our Chameleon Wrangler application, we have this UI here, it's our mood-o-meter. It's where we go to record how our various reptiles are feeling.
And it's in a popover, which means that it's automatically getting that awesome popover material backing.
And what we want when we're drawing over this backing material is for our content to really stand out on top of that varied background.
Something like this.
And we do that with an effect that we call vibrancy.
So, what is vibrancy? It's a blending mode that we apply to the content that uniformly lightens or darkens the content behind it. It's very similar to a color dodge or burn that you might have seen in your favorite photo editor or design tool.
But let's take a closer look.
Here we have a glyph that's drawing in about a medium gray, about a 50% gray, but at 100% opacity.
And when we apply the vibrant blending effect that we use against dark materials, which we call a lightening effect, we can see that it's not that the opacity has dropped on our glyph, but we're actually lightening the content behind it using the lightness of that gray value. And in fact, when we look at how this works on a range of gray values-- here we have swatches going from 0% to 100% gray, all totally opaque.
When we apply our lightening effect, we can see a number of interesting things have happened. Down on the bottom right side, we have 100% light, and because we have added the lightness of white to the content behind, it just remains white. There is nowhere further to go.
But on the top left where we were drawing black, there was no lightness to add, which means that it completely disappears. In fact, you wouldn't be able to see it if I didn't have an outline there.
And in-between we can see that we have varying degrees of lightening which we can use to establish a hierarchy of content in our application.
But where does this effect come from? Well, it's our old friend NSAppearance, it turns out.
We have two special vibrant NSAppearance objects, vibrantDark and vibrantLight and these are a complete package. Not only do they include the exact formula that we use for that lightening or darkening effect, but they also have a set of control artwork and color definitions that have been designed to work great with that blend mode.
But how does your code use it? Well, it's very simple.
In your NSView subclass, you can override the allowsVibrancy property to return true and the blending effect is going to automatically apply to your views drawing and also the drawing of all of its descendants.
Typically, when you're drawing in this vibrant context, you want to use one of the built-in label colors, depending on the prominence of your content.
Both vibrantDark and vibrantLight have great definitions for all four of these colors that allow you to establish that nice hierarchy.
However, you don't have to use these colors. You can use any color that you'd like, but we prefer to use non-grayscale colors.
Avoid non-grayscale colors because, if you use them, the blending effect is going to impact the design intent of your color and it's going to wash it out in a way that is not desirable.
I'll show you an example of that later.
So, revisiting our application, we can go ahead and override allowsVibrancy on our view and in this case we're going to just set it on the view that contains our entire meter in the entire popover.
And let's see what that looks like.
Well, our slider looks pretty good. It's exactly what we expected. But what happened to the faces? They're all washed out.
And what happened here is that when we set allowsVibrancy on the overall meter view, not only are we getting the vibrant blending on that view, but also both of these subviews.
And the fix here is pretty simple.
If we localize our definition of allowsVibrancy to just the part that's drawing the slider, we get exactly what we expected.
Our slider is drawing vibrantly and the colors in our face buttons look exactly the way that we wanted.
When you're drawing vibrantly, typically you'd want to apply vibrancy to only the leaf views that are drawing the content that you actually want to have vibrant.
And, if you have views that are drawing a mix of content, that means that you probably want to break your drawing out into separate sibling views that you can use to apply vibrancy at the granularity that you want.
Further, you should avoid overlapping vibrant and non-vibrant views.
If you do this, the blending modes can clash and you might find that some of your content is drawing with a blend mode that it didn't expect. Further, don't subclass Cocoa controls just to override allowsVibrancy.
I mentioned earlier that the vibrantLight and vibrantDark appearances have been designed with control artwork and colors that were designed specially for the blend mode and if you remove that blend mode, the contrast on that artwork is not going to be what you expected because we're using the blend mode to provide a lot of that pop against the material, so you should only override allows vibrancy if you're actually overriding drawing and you know what vibrant blend mode or non-vibrant blend mode is appropriate for the drawing that you're doing.
Next, I want to talk a little bit about background styles, specifically the ones that we use for selections.
So, here we have a pretty typical situation in a aqua Cocoa application.
In this case, it's a message from the Mail application and we can see that when we have a selection state, we need our content inside of this table row to invert to look good against that blue selection.
But when we add darkAqua into the mix, we can see that we can't just naively invert our content anymore. That's not going to work uniformly.
And so we need to describe these states semantically.
Now, if you're familiar with Cocoa, you've probably seen the NSView.BackgroundStyle enum and that includes a couple of cases, including light and dark, and NSTableView sets this automatically on the TableRowView, TableCellView, and also all of the controls that are immediate subviews of your TableCellView.
Now, traditionally, we have set the light background style on unselected rows and the dark background style on selected ones.
But, in the face of this brand-new, beautiful theme where the background is effectively always dark, these names don't make sense anymore and so we've renamed them to normal and emphasized, respectively.
And these are just more semantic descriptions that better match the way that these enum cases are used in a modern Cocoa application.
We also have some additional updates with background styles, including that TableView will now automatically set that background style recursively on all of the controls in your table row, not just the ones that are immediate subviews of your CellView.
And so, if you've been catching that background style and trying to forward it along to all these subviews because you wanted to use a stacked view or something for layout, you no longer have to do that on Mojave.
That's the applause of somebody who's done this manually. Thank you, I agree.
Further, all four of our label colors now automatically adapt to the background style, which means that you can just set up your content hierarchy once, describe it semantically, and it's going to look great in both of these contexts.
You can also use these emphasized variants manually and I'll give you an example. So, here we have something that looks a little bit like the icon view in Finder.
And we've got two labels that are ascribed with label color and secondary label color.
And we want to draw a custom selection behind them, so we've got this custom Bezier path-based selection, maybe we're filling it with alternate selected control color, and we want our labels to match the primary and secondary variants in this emphasized style.
And to get that is very simple.
All we have to do is set the background style to emphasized on both of our text fields and they're automatically going to provide this nice emphasized variant.
And the great thing is that now that we've described it this way, when we switch into Dark Mode, everything just works. We don't have to do anything special to support that. One final note on selections.
The selection material that you commonly see in sidebars, menus, and popovers now follows the accent color preference on Mojave.
And what that means is that if you're drawing a custom blue selection, it's not going to fit in.
Instead, you should use NSVisualEffectView.
It has a special selection material just for this and when you use this it's going to automatically follow the preference, as you expect.
Now, before I get into the exciting part, the tips and the tricks, I want to say a couple of words about backward deployment because we know that many of you, especially on the Mac, like to deploy your applications back to previous releases of macOS and it was important to us to make sure that you could adopt Dark Mode without necessarily compromising on your backward deployment.
And so I'm going to step through a couple of APIs and just examine them for backward deployment, starting with system colors.
So, here's a sampling of the system colors that we support that are dynamic for the appearance.
And what I want to highlight here is that the ones highlighted in green have been available since at least 10.10 Yosemite, many of them actually far further back. And that means that we think that you have a great vocabulary of colors available to you to describe more or less any UI that you'd like and that all supports backward deployment out of the box.
For custom colors, our modern preferred solution for defining them is asset catalogs and these are available back to 10.13.
Now, when you do specify dark variants for any of your assets, when you back deploy them, those dark variants are safely ignored on previous versions of the operating system, so that's a-- that's a solution that has backward deployment built right in.
But if you want to deploy back further than 10.13, you can use a technique like this where you write a custom color property.
And here we just encapsulate the availability check to use our asset catalog color on operating systems that support it and then we can go ahead and put in a hardcoded fallback color for those older operating systems.
Desktop-tinted materials is another great new thing in Mojave and if you want to address those materials directly with VisualEffectView, of course that's only available starting in 10.14, but we've been providing-- but we have classes that are providing these materials automatically, including Window, ScrollView, and TableView, which have been available since essentially the beginning of time.
In fact, some of these predate macOS 10.0.
And so, if you configure them correctly, they're going to on previous operating systems show that special NSColor which looks exactly the way that you would expect in previous versions and then when you run it on Mojave you're going to get that material automatically.
And of course NSBox, the custom style that allows you to set a fill color, deploys back to Leopard 10.5 and so does NSCollectionView.
And this works whether you're using the legacy NSCollectionView API or the modern one, although we'd prefer that you use the modern one.
Finally, enabling Dark Mode is generally gated on linking against the 10.14 SDK, but, as you can see, really, the tools that you need to develop a great Dark Mode application aren't necessarily specific to the 10.14 SDK and you could have developed one just using the 10.13 SDK that you have today.
And so, if you have a situation where you can't necessarily update your SDK, we have an Info.plist key that you can use to opt-in to Dark Mode.
It's called NSRequiresAquaSystemAppearance and if you set that to NO explicitly, then that's going to enable Dark Mode even if you're linking on an earlier SDK, although we very strongly prefer that you update your SDK. It's a far better solution.
You can also set this key to YES to disable it temporarily-- and I want to emphasize temporarily.
This is a key that you can use to give yourself time to really build a great polished update for supporting Dark Mode.
Finally, some tips and tricks.
First of all, when you're updating your application, one of the greatest things that you can do is just audit your use of NSColor just by searching through your code base and seeing where you're using it.
And you're going to find a couple situations that you can use to upgrade to make your Dark Mode experience a lot better. And so, for example here, we can find places where we're using named colors that are not dynamic and also colors that have hardcoded components.
And when we encounter these kinds of situations, we can look at these and decide one of two things.
One, maybe there is a built-in system color that describes what I'm going for and is fully dynamic for the appearance.
Or, two, this is a custom color that I think is really important to be specific to my application.
And so the first case is pretty straightforward. We were using black color for this label and we can just switch that to labelColor and that's going to be fully dynamic and do what we expect.
But in the second case, we might decide that this color is actually really special to our app and that's a really great candidate for moving into the asset catalog.
Not only does this clean up our code because we get all of these magic numbers out of our code and into a data-driven source, but we can also then set a dark variant for that color and so we get great Dark Mode support built in.
Another common source of issues is offscreen drawing.
To do offscreen drawing, you have to make sure that you're being sensitive to the appearance and also other drawing conditions.
One really common case of this is using the NSImage lockFocus API to try and draw custom NSImages.
In this case, we're going to go ahead and try and draw this badged image where we have a base image and we're applying a badge because something new is happening with our lizard.
And, in this case, we're creating an NSImage, calling lockFocus on it, and then doing our drawing.
And the problem with this is that once we've used lockFocus, we lose a lot of the semantics. We just have a single bitmap representation.
And so if the appearance changes or if many other conditions change, including say the backing scale factor because you've moved your window from a Retina display to a non-Retina display, suddenly this drawing is going to be stale.
So, a better solution is to use the block-based image-- image initializer, NSImage size flipped drawing handler.
And we can just do the exact same drawing that we were doing before, but inside of this block.
And when you assign this kind of image to an NSImageView, you're automatically going to have this block rerun when the appearance changes, scale factor changes, color gamut changes-- anything changes, essentially.
And so that's great news because if our, say, badge fill color is a dynamic color, it's going to always resolve against the correct appearance.
There are a couple of other ways that you might be doing offscreen drawing.
You might be making custom bitmap graphics contexts using NSGraphicsContext or CGBitmapContext.
And, depending on what you're doing, these might also be great candidates for replacing with a block-based NSImage.
Further, if you're using the NSView cacheDisplay in Rect method to cache your image to a bitmap rep, just be aware that this method is not going to capture some of our more advanced rendering techniques like materials and blurs and it's also just another way that you can produce drawing that goes stale when the appearance changes, so be aware of that. Here's another situation that you might find yourself running into.
If you have an NSAttributedString or NSTextStorage and you're manipulating those attributes manually-- say I am in this case, I've just set my attributes to just be a dictionary with a font in it-- you might find that this happens. Your text is drawing black even when you switch into Dark Mode and what has happened here? Well, we're missing a foreground color attribute and when the text drawing engine encounters a range of attributed strings that doesn't have a foreground attribute, it defaults to black. And this is what it has always defaulted to and it's going to continue to be the default for compatibility.
So, one way to fix this is to set a foreground color explicitly to one of our dynamic system colors, and that's going to do what you expect.
But a better alternative is that if you're doing manual attributed string drawing, you should switch to a Cocoa control, like an NSTextField, which does this for you automatically, or, if you're manipulating the storage of a textView, we have new API called performValidatedReplacement on textView that does a nice thing for you.
If you go ahead and replace a string with an attributed string in your textView, it will fill in any missing attributes with the typing attributes from the textView, so that way you can go ahead and specify your new attributed string without having to manually merge all your attributes together.
Here's something else that we've encountered in a couple places, which is appearances that are set in Interface Builder.
So, if you're going ahead and building and debugging your application and you find that there's some part of your app that just isn't switching, you might have this in your Interface Builder. A hardcoded aqua appearance. And it's easy to miss, because before today, essentially, you were always running under aqua, so you didn't notice it.
And the fix for this is easy.
If you set this back to the Inherited option in the pop-up menu, your view's going to automatically inherit from its ancestor.
An extra special case of this is NSVisualEffectView.
It's very likely that if you have a VisualEffectView in Interface Builder or even in code, you're setting one of the two vibrant appearances on it and the great news is that in macOS 10.14 this is no longer necessary.
NSVisualEffectView will automatically pick the right vibrant appearance based on the appearance it's inheriting.
So, if it inherits darkAqua, it's going to choose vibrantDark and if it inherits aqua, it'll choose vibrantLight. And so the fix for this is easy. In Interface builder, you can set this to inherited and then in code you can set the appearance to nil or just delete your override.
Interface-- speaking of Interface Builder, Interface Builder is a great tool for designing and previewing your views visually.
And so, for example, here I have a view that is actually a custom view using IB designable.
So, I'm rendering a gradient here and I can see it right here in the canvas.
And, by default, my canvas is previewing my custom designable view using the canvas's appearance, in this case dark.
But down at the bottom, we have a new toggle that lets you go ahead and set it to the light appearance so that you can preview the way that your view looks in either appearance.
And thanks to Interface Builder's great support for asset catalog colors, we can actually use our custom asset catalog colors, which have dark and light variants, and we can preview them in the canvas live.
And if you see there's a little arrow button built into that pop-up button and you can use that to follow it and go straight to the definition in your asset catalog, so you can see live as you're changing it.
And you can do this all without even building and recompiling. When you do build and run, you're going to see a new item in your Debug Bar and it produces a menu that allows you to choose the appearance for your application.
And this is really handy for previewing your app in various appearances without having to go and reconfigure your entire system.
Not only can you choose light and dark, but you can also choose the high contrast variants and test those as well.
And, if you have a Touch Bar Mac, this appears in the expanded Debug Bar as well, so you can do this without even leaving your app to go back to Xcode.
Finally, I want to talk about one last tool in Xcode that is really great for debugging your Dark Mode applications.
So, here we have our app and really things are looking pretty good. There's nothing out of place, but I find that when I scroll and rubber band a bit, oh, I'm revealing something that I didn't expect. There's a light background hiding back there somewhere, but it's hard to see without doing that little scroll gesture.
And this is a great case for using the View Debugger.
Using the View Debugger's expanded 3D view, the view that's drawing unexpectedly is really easy to spot.
And in this case, we can see that although our collection view was drawing the background that we expected, the scroll view behind it still has a light background for some reason. And when we select it, we can use the Inspector to see how it's being configured. And in this case, we can verify that, yeah, it's just drawing a hardcoded white color and that's a really easy fix.
The View Debugger has made a number of enhancements in Xcode 10 that are great for debugging Dark Mode applications, including colors. They can now show you the names of colors, both dynamic system colors and your asset catalog colors, so you can identify where these RGB components are coming from, and it'll show you the configuration of your view for NSAppearance, including the appearance that it's going to draw with as well as whether there's any local overrides of NSAppearance on that object.
So, we have covered an awful lot of content and so let's rewind and make sure that we remember it all.
We started off with NSAppearance and leveraging it effectively to draw your custom views that adapt based on the theme.
Then we learned how to add depth and beauty to our UI using our new and updated palette of NSVisualEffectView materials.
We talked about drawing in a couple of interesting contexts, both vibrancy and selections, and then we walked through some of the great ways that Xcode can help you design and debug your Dark Mode applications.
As always, you can go to developer.apple.com to re-watch the video for this talk and see any related resources and we have labs today.
We have a special Cocoa and Dark Mode Lab, it's at 2:00, and not only will we have Cocoa engineers onsite to help you with your code, but we'll also have human interface designers onsite to help you with your design questions as well.
So, go get lunch, think about Dark Mode the entire time, and then come see us.
And then, finally, we have an additional Cocoa Lab on Friday at 11:00 as well.
All right. Thank you very much.
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.