WWDC2018 Session 218

Transcript

[ Music ]
>> Good morning, everyone.
[ Applause ]
Wow.
Thank you.
Welcome to Advanced Dark Mode.
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
[phonetic] 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.
OK.
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.
OK.
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.
That's great.
All right.
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.
All right.
Let's look at it now.
Looks a lot better.
It's desktop-tinted.
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.
[ Applause ]
Jeff.
>> 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.
That's vibrancy.
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.
[ Applause ]