Transcript
[ Music ]
[ Applause ]
>> Good morning.
[ Applause ]
Good morning.
And welcome to SwiftUI
Essentials.
My name is Matt Ricketson and I
work on SwiftUI and later I'll
be joined by my colleague
Taylor.
So what do you all think of
SwiftUI so far?
[ Applause ]
Me too. I'm incredibly excited
to talk to you today about
SwiftUI.
Now we have a lot to cover in
this session, so let's dive
right in.
SwiftUI is a new framework that
is designed to give you the
shortest path to building a
great app.
And that means giving you the
shortest path to building great
user interfaces.
But even though SwiftUI is a new
framework, a lot of it will
already look familiar to you.
And that's because it has all of
the basic components that you'd
expect from a UI framework.
It has controls like buttons and
text fields.
It has layout containers like
stacks and lists.
It has drawing, animations and
gestures.
And SwiftUI even embraces
platform-specific concepts like
menus on the Mac, the Digital
Crown on Apple Watch, and the
Siri remote on Apple TV.
And so the takeaway here is that
we're not trying to reinvent the
wheel with SwiftUI.
But as we all know, the reality
is that just knowing how to use
these kinds of components is not
what it takes to build a great
app, because a great app also
needs to account for these kinds
of things.
It needs to be accessible and
work with features like dynamic
type.
It needs to adapt to different
devices and screen sizes and
input types.
And it needs to come alive with
things like interactive
animations and support for
system features like Dark Mode
and Drag and Drop.
These are the kinds of things
that help your app to reach the
largest possible audience and
also help keep it feeling
modern.
Now we all know that even this
though is not the whole picture,
because of course you also add
in your own unique features that
make your apps stand out from
the crowd.
So I just want to take a moment
to step back and acknowledge
that this is a lot of stuff to
have to learn.
It's a lot of stuff to have to
code and maintain, and so how
can SwiftUI help you with all
this?
Well, think about your own apps
for a moment.
First, you have those basic
features that everyone expects
from your app, like controls and
navigation, being accessible and
adapting your layout to
different devices.
We need to do these things and
we need to do them right in
order to build a really great
app.
But then there are those
exciting custom features that
are unique to your app.
And these are also the fun
features, the features that we
pour our passion into, the
features that make us feel proud
of what we've built.
And so the goal of SwiftUI is
pretty simple: we want you to
spend as much of your time as
possible on that fun stuff and
less time on the basic stuff,
but without compromising on
quality.
And this is what we mean by
giving you the shortest path to
a great app.
Because all of you are building
great apps already.
We just want to help you get
there a little bit faster.
This session is about giving you
a better understanding of
SwiftUI.
We're going to look at some
code, but we're also going to
talk about SwiftUI's design and
how it helps you build better
apps.
By the end of this session,
you'll be able to build a
complete user interface with
SwiftUI.
And we're going to start by
covering the basics of views and
modifiers.
And for that we'll need an
example.
And I always try to pick an
example that I care about to
help motivate me.
Now if any of you have been on
the internet lately, you've
probably read about what
Millennials like myself consider
to be the most important part of
our lives.
That's right, avocado toast.
[ Laughter ]
[ Applause ]
We've got some Millennials in
the audience.
So today we're going to build an
app for ordering avocado toast.
And I've already done a little
bit of work on it already and it
looks a little like this.
It's a simple form that lets me
quickly order just what I want
right from my phone.
Now this is not much so far,
clearly, but we're going to
build on this throughout the
talk.
But before we dive into the
code, I want to talk a little
bit about views.
And that's because views are the
basic building blocks of user
interfaces.
And they're important to
everything that we do in
SwiftUI.
If you've ever used another UI
framework before like UIKit or
AppKit, you've probably already
heard of the term view.
SwiftUI also has views and they
serve the same primary role as
they do in those frameworks.
Which is that at a high level, a
view is just something that
defines a piece of your UI.
When you look at an app,
everything that you see is
defined by a view.
Individual controls are views.
The containers holding them are
also views.
And in fact, every single pixel
that you see onscreen can be
traced back in some way to a
view.
And we build user interfaces by
composing these views into a
hierarchy of containment.
From the containers at the root,
to the text, images and shapes
that are at the bottom.
Now if you've used UIKit or
AppKit before, this picture
should look familiar to you.
And the important thing to
understand is that this is also
true of views in SwiftUI.
Where SwiftUI may be different
than what you're used to is in
the way that views are expressed
in code.
So let's look at some code.
In our example app, we just have
a vertical stack of controls and
text.
And it's easy to see that just
by reading the code.
But in fact you'll notice how
closely the code on the left
matches the equivalent view
hierarchy diagram on the right.
We see that in the stack at the
root, to the text and controls
contained in the stack.
To the individual text labels
contained in each of our
controls.
Now what you don't see is calls
to functions like Add subviews
anywhere.
Because instead of building up
our view hierarchy piece by
piece, we initialize it as a
complete, composed structure.
This is because SwiftUI defines
its views declaratively as
opposed to imperatively.
And I can't think of a better
analogy to help explain these
concepts than of course with
avocado toast.
So let's try making avocado
toast imperatively.
Imperative code involves
building a result by sending
explicit commands.
That's sort of like teaching a
friend how to make avocado toast
over the phone.
You start by telling them what
ingredients to get and what
equipment they'll need, then you
start guiding them through
making the toast and cutting the
avocado and all these
instructions start getting a
little tedious.
And if your friend messes up any
little step like forgetting to
toast the bread, then the final
result is ruined.
Now let's compare that to making
avocado toast declaratively.
Declarative code involves
building a result by describing
what you want but letting
someone else figure out how to
make it for you.
That's sort of like ordering
avocado toast from an avocado
artisan.
Luckily, we have a lot of those
in California.
Now all you have to do is say
exactly what you want.
You can even throw in a custom
instruction.
And that's all there is to it.
And because an expert is making
it for us, we're guaranteed to
get a high-quality result.
Now going back to our code,
SwiftUI is serving that role of
the expert ready to assist you.
In code, we declare the
hierarchical relationships
between our views by
initializing a structure that
encodes those relationships.
And SwiftUI does the hard work
of translating your views into a
rendered result onscreen.
Now there's a lot more to say
about that, but for now let's
just get used to the syntax in
the code.
And we'll start with container
views.
container views are declared as
a composition of other views
serving as their content.
Those Content views are declared
within a special kind of closer
known as a view builder.
For example, we already saw
VStack or Vertical Stack which
is an example of one of these
containers.
view Builders allow us to write
declarative code in the body of
the closure.
Instead of calling a function
like AddSubViews, we can just
list out our contents within the
closure.
To see a little bit more about
how this works, let's take a
look at the actual API for
VStack.
You can see the content
parameter defined as a closure
but marked with this ViewBuilder
attribute.
The Swift Compiler knows how to
translate a closure marked by
this attribute into a new
closure that returns a single
view representing all of the
contents within our stack.
This is an example of SwiftUI
using the power of Swift to help
you write less code.
views like VStack can also take
other parameters in addition to
their content.
For example, we could configure
our VStack to align its content
along its leading edge instead
of using the default center
alignment.
Taken together, this is a really
nice and natural syntax that
lets us use braces and
indentation to differentiate our
container views and their
configuration from the contents
inside of them.
And we also follow the syntax
for many controls since most
controls in SwiftUI are also
containers.
You saw this in our example app.
In each case here, our controls
define a piece of text serving
as their label which describes
their purpose.
Now we can put more than just
text here.
We can put any kind of view.
And we'll go into more depth on
that later in the talk.
Now another kind of syntax you
see here are those dollar signs
preceding the arguments to our
Toggles and stepper.
The leading dollar sign
indicates that we're passing a
binding to the control instead
of just a normal value.
So what are bindings?
In our example app, our stepper
is contained within a view that
depends on persistent state to
track the current order.
It declares a property for its
order using a state attribute.
When SwiftUI sees a property
marked with this attribute, it
automatically creates and
manages persistent state behind
the scenes and then exposes the
value of that state through this
property.
In this case, our state contains
a struct that I defined myself
that represents all of our order
information.
If we just want to read or write
to the data in our state, it's
really easy.
We can just read or write to a
property directly.
And we did that here when we
made the label for our stepper.
However, a stepper also needs to
be able to edit the state when
its buttons are tapped.
And we use this dollar sign
prefix to indicate that we
should pass a binding to that
Quantity Property in our state
instead of just passing a
read-only value.
A binding is a kind of managed
reference that allows one view
to edit the state of another
view.
Now to learn more about state
and bindings and how to manage
all other kinds of data
dependencies that you'll use in
your app, I highly recommend
that you watch the Data Flow
Through SwiftUI talk.
But for now, the important thing
to remember is that if you ever
see a property attribute like
state that usually represents
some kind of data dependency
that SwiftUI is managing on your
behalf behind the scenes.
And if you ever see a dollar
sign prefix, that usually means
that we're passing a binding to
another view.
Now going back to our example
app, there's one more important
piece of syntax that we haven't
covered yet.
And you can see it up there at
the top where we set the font
for our title.
Let's zoom in on that.
First we initialized our text,
which again is just another kind
of view in SwiftUI.
Then we called a method on the
text named font and passed it a
system-defined text style.
This kind of method is known as
a modifier in SwiftUI.
And a modifier is just a method
that creates a new view from an
existing view.
Let's see what I mean.
This is what our UI would have
looked like without the font
modifier, in which case our
title would have rendered with
just the default body font.
This is what the view hierarchy
diagram looks like.
We just see our text contained
by our VStack.
When the text is modified, a new
view is inserted that wraps our
existing text.
The new view tells SwiftUI to
render that text with its new
font.
These modifiers can even be
chained together.
For example, we could change the
text color of our title by
adding a foreground color
modifier.
This adds another view into the
view Tree that wraps our font
modifier view.
Now clearly our view hierarchy
is starting to get bigger pretty
quickly.
And for the experienced UI
programmers among you, this may
be setting off some internal
alarm bells.
Because over the years we've
trained ourselves to optimize
the performance of our apps by
keeping our view hierarchies as
small and light as possible.
But remember, we're writing
declarative code.
And SwiftUI is our expert chef
taking our views and skillfully
producing a rendered result
according to just what we
ordered.
And so even though we had to
wrap our text in multiple
wrapper views, SwiftUI collapses
that down behind the scenes into
an efficient data structure that
is then used by the render
system.
And without having to worry
about the performance impact,
you'll find that this chaining
modifier syntax actually
provides a lot of really nice
benefits.
For example, modifier chains
enforce a deterministic ordering
of visual effects.
So here we have a piece of text
with a green background.
But the text is looking a little
cramped, so let's try expanding
that background by adding some
padding around our text.
So we added the padding modifier
and you can see it adding a new
view to our view hierarchy.
But nothing changed onscreen.
In fact, the padding is there;
we just can't see it.
Looking at the code, our
background modifier is only
wrapping our text, not our
padding.
Which means that the padding
gets applied outside of our
background.
And luckily, it's really easy to
fix this by just moving that
background modifier to wrap both
the text and the padding
instead.
Now let's take a step back and
appreciate what we just did
there.
Imagine if padding and
background were properties on
our text instead of separate
modifiers.
In that case, we would have no
way to know which order they get
applied in without trial and
error or reading documentation.
Instead, by chaining modifiers
together like this, we make that
order explicit.
And we also make it super easy
to customize like we just did.
Now another benefit of these
modifiers is that they can be
shared across views.
For example, here we've applied
an opacity effect to multiple
different kinds of controls.
And we can even apply that
opacity to the entire stack
instead of each individual
control.
None of these views had to
define their own opacity
property.
Which means that they're free to
have simpler, more focused
interfaces of their own.
And this gets at a general
principle of SwiftUI.
Which is to prefer smaller,
single-purpose views.
These kinds of simpler views are
easier to understand and also
easier to maintain over time.
And once you have all of these
little views, you can compose
them together to create bigger,
more complex views.
The entire SwiftUI framework is
oriented around composition of
small pieces and you should
organize your code in the same
way.
So you can start with something
simple like our text.
You can modify that into
something better.
And you can compose that
together to build something
great.
You know, like an app for
avocado toast.
And personally I can't wait to
see the kinds of user interfaces
that all of you are going to
build with SwiftUI.
But before you can do that,
we're first going to need to
know how to build our own custom
views.
And so let's build something new
now.
Looking at our app, I'd really
love to be able to see a history
of my previous orders.
I've already sketched out a
design.
It's just a simple list showing
a summary of each order and some
icons for the toppings that I
chose to include.
I've already gotten started on
the code, so let's just go
through this quickly step by
step.
First, I declared a new view
called OrderHistory as a struct
that conforms to the view
protocol.
We'll come back to that.
My view has a single input
property, previousOrders, which
is just a collection of all of
my order information.
My view has a computed property
called body returning the
contents of the view.
And the sum keyword that we use
here is a Swift feature that
lets Swift infer our return type
automatically.
Our body property returns a list
which generates its contents by
mapping each of our previous
orders into a collection of new
views, one for each order using
another one of those trailing
ViewBuilders.
So now that we understand this
code, let's go back and take a
deeper dive and learn why
SwiftUI defines custom views in
this way.
And let's start with how views
are structs that conform to the
view protocol.
If you're coming from UIKit or
AppKitt, you've probably gotten
used to views being defined as
classes that inherit from a
common view superclass instead
of as structs conforming to
protocols.
For example, custom views in
UIKit inherit from the UIView
superclass.
And UIView defines storage for
common view properties like
alpha and backgroundColor.
Let's imagine we built our
OrderHistory using UIKit instead
of SwiftUI.
Our Custom View would inherit
the stored properties of UIView
as well as adding more
properties for its own custom
behavior.
So how is SwiftUI different than
this?
Well, remember that in SwiftUI
we represent those same kinds of
common view properties as
separate modifiers instead, like
we did for opacity and
background.
And each of these modifiers
creates their own view.
And this means that the storage
for those properties is
distributed across our view
hierarchy in each of these
modifier views instead of being
inherited by every individual
view.
Now this allows our views to be
lighter weight, optimizing their
storage for just their unique
purpose.
And in this world, it makes a
lot of sense that view just
becomes a protocol because it's
no longer needing to serve a
common storage template for all
of your views.
But what does this view protocol
actually do?
Well, let's remember our
conceptual definition of a view.
Which is that a view defines a
piece of our UI and we build
bigger views by composing
together smaller views.
And that's all that the view
protocol does.
It defines a piece of our view
hierarchy, giving it a name so
that it can be composed and
reused across your entire app.
And each concrete type of view
is just an encapsulation of some
other view representing its
contents in its body property
and all of the inputs required
to create that view represented
by its properties.
Now the actual protocol just
defines that one body property
returning just another kind of
view.
But looking at this definition
for a second, some of you may be
asking yourselves, isn't that
kind of recursive?
If I have some view and it
defines as body as another kind
of view, well, then that view is
going to define its body as
another kind of view.
And it has to end somewhere,
right?
It can't just go on forever.
So the reason this works is
because SwiftUI provides many
kinds of primitive views,
meaning views that don't have
any contents of their own and
that represent those atomic
building blocks on which all
other views are built.
We've already seen text.
An image is another example of a
primitive view.
SwiftUI also offers primitives
for drawing like Color and
Shape, as well as layout
primitives like Spacer.
In fact, you can do some pretty
sophisticated drawing just using
primitive views in SwiftUI.
And to learn more about that,
you should definitely watch
Building Custom Views in SwiftUI
talk.
Our example uses text.
But our list actually adds in
its own primitive views that you
can see as the dividers in
between each of our rows.
Now we also saw that our Custom
View is defined as a struct
instead of a class.
And this goes back to how views
are defined declaratively in
SwiftUI.
In this case, that means our
views are not persistent objects
that we update over time using
imperative event-based code.
Instead, our views are defined
declaratively as a function of
their inputs.
So whenever one of our inputs
changes, SwiftUI will call our
body property again to fetch an
updated version of our view.
Now List that we're using here
-- List is a great example of
the power of declarative code.
If our previousOrders collection
changes, SwiftUI will compare
the old and new versions of our
list and efficiently update the
rendered result onscreen just
based on what's changed.
For example, I've been working
on cloud sync for my app.
And it's really important to me
that all of my avocado toast
data is available on all of my
devices.
So let's see what happens if
another device starts adding and
removing orders from our
history.
What you see on the right is
SwiftUI automatically diffing
the changes in our collection
and synthesizing insertions and
deletions and then rendering
them with appropriate default
animations.
And this is all functionality
that you get for free without
writing any additional code.
[ Applause ]
It's pretty awesome.
And the reason this works is
because you don't have to manage
that persistent render state
yourselves.
Instead, you can just generate
new values for your view based
on your current data in that
body property.
And you can let SwiftUI generate
the necessary changes between
those two versions on your
behalf.
And that's the power of
declarative code.
So let's build out the rest of
our orderHistory view.
And if you recall, our original
design included these icons for
any extra toppings that I
included in my order, like salt
and red pepper flakes.
So let's start by showing that
icon for salt.
First, we'll add a horizontal
stack with a Spacer after our
text.
And then I'll show my SaltIcon
view but only if our order
contains salt.
As you can see in the code here,
that ViewBuilder syntax that we
talked about earlier, it lets us
use natural control flow like if
statements to declaratively
define when a view should be
included in our stack.
And using if statements like
this in our declarative code
feels really natural.
But there are also other ways to
write conditional code within
your views.
And it's important to choose the
right tool to get the correct
result onscreen.
So let's look at a quick example
to see what I mean.
I built another screen for our
app which lets you choose
between a normal and flipped
AppIcon.
And my first pass at this was
writing a custom view that takes
a flipped state as an input and
conditionally applies a rotation
modifier based on my state.
However, this produces an ugly
crossfade animation when we
actually try to flip that icon.
This is because our code is
telling SwiftUI to switch
between two different kinds of
views.
A view wrapped in that rotation
modifier versus our AppIcon just
by itself.
And by default, SwiftUI fades in
and out views when they're added
and removed.
Which is why we get this
crossfade effect.
Now instead I'd really like that
icon to rotate when it's
flipped.
And so to do that, I define a
single view with a single
rotationEffect modifier and
conditonalize its input based on
our state.
By defining our condition inside
of our modifier, SwiftUI can
provide a better default
animation, rotating our icon to
the new orientation.
And the lesson here is that you
should try to push your
conditions into your modifiers
as much as possible.
Because that will help SwiftUI
detect those changes and give
you better animations.
That if statement syntax that we
saw earlier, that's really great
if your intention is to actually
add or remove views from your
hierarchy.
So going back to our example
app, our orderHistory view is
starting to get a little bit
big.
So it would be nice to start
breaking this down into some
smaller pieces.
So let's try factoring out the
code for each List row into its
own custom view.
First, I'm going to create a new
custom view called OrderCell.
Now I'll need a body for this
view, and luckily we've pretty
much already built that just
within our lists in our
OrderHistory view.
So let's move that code over.
Our OrderCell requires input
data in order to generate its
body.
So we're also going to need to
add a property to represent
that.
And finally, we'll finish up by
creating an instance of our new
view for each row within our
list.
And the takeaway here is that
it's really easy to break down
your UI into smaller pieces and
to factor out code into new
views.
And remember, with declarative
code, adding a new wrapper view
is effectively free since
SwiftUI will optimize it down
behind the scenes.
And so the important thing here
is that you no longer have to
compromise between organizing
your view code the way that
makes the most sense to you and
getting the best performance
from your app.
[ Applause ]
So let's finish by including
that final icon for red pepper
flakes.
And it's easy to do that just by
adding another condition like we
did before.
Now this works but it doesn't
seem very scalable.
If we add new toppings in the
future, we'll have to add them
with new conditions into our
code.
What would be really great
instead would be to
conditionally generate a
collection of icons from our
order data.
To generate a collection of
views, we can use a ForEach
view.
Just like our List, ForEach
takes a collection of data and a
ViewBuilder that maps each data
item into its own view.
But unlike List, ForEach doesn't
add any visual effects of its
own.
Instead, it just adds its own
contents to its container.
So this code is a lot better
because now our order history
will automatically support new
toppings in the future without
us having to add any more code
to our view.
For example, we could add a
third icon for eggs.
So taking a step back, it's
pretty amazing how much
functionality we were able to
just build with just about a
dozen or so lines of code.
And what's even more amazing is
all of the code that we didn't
have to write.
We already saw how SwiftUI
automatically handled changing
data, even inserting default
animations when our data is
added and removed.
But I didn't mention that our
app also adapts to dynamic type.
And it even supports Dark Mode.
And we got all of this support
for free without writing any
additional code.
[ Applause ]
This is pretty great and this is
what we mean by SwiftUI giving
you that shorter path to a great
app.
So that's a lesson on building
custom views with SwiftUI.
And now I'd like to invite up my
colleague Taylor to talk to you
about how to take full advantage
of the views that SwiftUI
provides for you out of the box.
Thanks.
[ Applause ]
>> Thank you, Matt.
Hello, everybody.
At this point, we have a pretty
good start to our app, with Matt
building out the initial order
form and history screens.
But one thing that stands out is
that this doesn't quite look
like iOS apps we're used to.
They're usually not these simple
vertical stacks of controls.
And typically, this type of UI
looks something more like you'd
see on the right.
And one of the biggest
differences is the container
around the controls themselves
having this standardized group
list style.
Now in SwiftUI we refer to this
as a form.
And a form is a container just
like VStack, but one built
specifically for building these
sections of heterogeneous
controls, giving the overall
result a standard look and feel
no matter what the platform.
Now we've already defined the
exact set of functionalities we
want in our app.
The title, Toggles, stepper and
button.
And all we're doing is changing
the container itself from the
existing VStack into a form.
And then we can easily add in
some sections to divide up that
content.
Now just as Matt previously
discussed, our code continues to
reflect the resulting UI.
And since the core definition of
our controls didn't change, our
code really didn't have to
either.
Just by changing the container
from a VStack to a form resulted
in the controls automatically
adapting to that context.
From the overall background and
scrollability to the lines
separating each of the controls,
to even the styling of things
like button.
This is yet again SwiftUI taking
care of the details for what
exactly it takes to render those
elements, and allowing us to
focus on the functionality of
our app.
Now one subtle change that
happened isn't visible from this
static screenshot.
Focusing on the buttons, you can
see that the alignment, padding
and decoration has all changed
around the button, but the press
state has even taken on the
special full bleed effect that
you would expect from this type
of UI, all the while showing the
same exact definition of being a
button.
Like you might expect, this same
definition works in other
contexts or other platforms,
having a wide variety of
possible looks and feel.
button also demonstrates the
same inherent ability for
composability that we've seen in
other views.
The label is of course not
constrained to just being a text
but could also be an image.
It really could be any type of
view that we could define, even
an explicit vertical stack of an
image and a text.
[ Applause ]
And this inherent composability
enables a wide variety of
possibilities while at the same
time enabling button to be
distilled down to two
fundamental properties.
The action it performs when
activated and the label
describing what that action is.
And that's the entire API
surface of button.
This is of course not to say
that these are the only two ways
that buttons can be customized.
Like we saw before and will
continue to see, both context
and modifiers enable adding many
more rich behaviors from
disabled state to the styling of
the button to even control sizes
on macOS.
But this core definition plus
adaptive behaviors enables any
type of button.
And over time and across the
different platforms, we've seen
a lot of different buttons.
Not only did they vary based on
how they look but also in how we
interact with them, from a click
to a tap, to being selected
using the switch control or the
Siri Remote, but they can all be
distilled down to having an
action and a label.
Now just like button, every
control in SwiftUI carries the
same ability to have this
adaptive behavior.
Controls describe the purpose or
the role that they serve instead
of just how they look.
And this allows them to be
reused across these different
contexts and platforms and adapt
to those situations.
And this also helps them have
that smaller API surface catered
to that exact role.
And at the same time still
having fewer controls rather
than need a control for every
context you might need to use it
in.
And all the while still enabling
really powerful customization
such as completely redefining
how buttons should look in your
app.
Now we saw how this adaptivity
allowed us to quickly transform
from a simple stack of controls
into the standard look and feel
of a system form.
But this same adaptivity also
enables us to take these
controls to other platforms such
as the Watch, so we can quickly
order our toast on the go.
Now the other control we're
already using is Toggle.
And you've already seen how
Toggle in SwiftUI is more than
just a literal switch.
And this is true regardless of
the platform it's on.
And like button, Toggle has two
fundamental properties, whether
it's on or off, and the label
describing the overall purpose
of the Toggle.
And again, that's reflected in
the construction itself.
Now one notable difference from
button is that it doesn't take
an action, but instead takes a
binding to a Boolean value.
And this binding is a direct
read/write connection to some
piece of state or model in your
application and allows the
Toggle to reflect and update
that without manually needing to
respond to an action, pull the
value out and then set it in
your model.
It takes care of it all for
yourself.
[ Applause ]
Now Toggle and the other
controls are also adaptive in
one other very important way.
For some people, UI's are a
visual experience while others
might predominantly use their
other senses to experience that
exact same UI.
For instance, people with
impaired vision are able to use
VoiceOver to navigate and
interact with your app using
audio.
And for those of you who haven't
heard it, this is what it sounds
like to begin using VoiceOver.
>> VoiceOver On.
>> Now VoiceOver is just one of
the system-wide features that
are able to take your UI and
surface it in these alternate
forms.
And because Toggle and the other
controls are defined based on
their purpose and include that
human interpretable label, they
can automatically adapt for
these features.
So when we navigate to this
Toggle using VoiceOver --
>> Include Salt, Switch button,
On Double-Tap to Toggle Setting.
>> It is able to reflect that
same label.
And this is true even when the
label isn't text.
Now for images, if the image
name isn't descriptive enough,
you can explicitly provide a
label directly alongside the
image.
And of course even for
completely custom --
[ Applause ]
It's really exciting, yeah.
[ Applause ]
And of course even for
completely custom views, you can
always explicitly provide the
label using the accessibility
label modifier.
Now in addition to VoiceOver,
this information also admits use
for other features, like the new
Voice Control on iOS and macOS
so that we can say, "Tap Include
Salt," and our UI behaves as we
expect.
And making sure your app is
accessible means it will work
with all these different
technologies and means that
everyone can use your app.
And SwiftUI is here to help.
There's a great talk this year
that will go into a lot more
detail about how you can make
sure that your SwiftUI app is
fully accessible.
Now at this point we've been
able to quickly build up this
initial basic interface that has
all the behaviors we expect:
dynamic type, Dark Mode and
accessibility.
But we've really only added a
few customization options for
the toast itself.
And of course everyone knows
that a professional artisanal
toast repertoire comes with a
variety of different bread
types, methods to prepare the
avocado and of course a variety
of spreads and add-ons.
To add in these more advanced
configuration options, we can
look for some inspiration from
the flexibility that is macOS.
Or we might want to have a
little utility window to allow
us to order toast right from our
desk.
You can see here that the
existing controls we're already
using take on the expected look
for macOS -- the Toggles, the
stepper, the button.
But we also have a few
additional controls that allow
us to pick from the type of
bread, the spread to add, and
how to prepare the avocado.
Now these are all examples of
the Picker control in SwiftUI.
Picker is built for the purpose
of selecting one value out of a
set of options.
Now Picker is obviously a little
more complicated than the other
controls and in fact has three
core properties instead of two.
The options that you can pick
from, the current selection from
those options and the label
describing the overall purpose
of the Picker.
Now the selection is a binding,
just like Toggles is on
property.
Which allows us to directly
connect it again to our modeler
state.
And the type of this binding
corresponds to the tag values
associated with each of these
options.
When one of the options is
selected, that tag value is
written back into the selection
and back into our model, all
with no work.
Now of course Pickers on macOS
don't always manifest as pop-up
buttons.
In this single window, we can
see two different styles of
Picker, both a pop-up button and
a radio group.
While SwiftUI automatically
provides a default style that's
adaptive to where controls are
used, controls also inherently
have the ability to customize
their styling, both to
system-provided styles and even
custom-built ones.
In this case, we want to
override the default style and
impose an explicit radio group
since we know that we are only
picking from two options.
Now we can consider doing the
same for our spreads.
But what might start out as a
humble set of four possible
spreads could quickly grow into
a wide variety.
So when it comes to building our
Picker, we obviously wouldn't
want to splay out each of these
options one by one, just as we
wouldn't want to build a UI that
displays them all as radio
buttons.
We've already seen using ForEach
to build data-driven views.
And since each of these options
are views themselves, we can use
it here as well.
This is a lot better.
Here we're going through each of
the cases of spread and creating
a new option with the spread's
name and the spread itself as
the tag.
Now --
[ Applause ]
Now obviously Pickers exist on
more than just macOS.
And then isolation -- a Picker
on iOS looks like the
traditional wheel-style Picker.
However, since we're building up
a form, SwiftUI will
automatically adapt Picker to
take on another really common
style of this type of UI.
Here we can see that the spread
Picker is now represented by a
navigation row displaying both
its label and currently selected
value.
Tapping on that row brings us to
a list of all of our options.
And tapping one of those selects
it and brings us back.
[ Applause ]
You stole my punch line.
This is SwiftUI taking care and
creating that entire interaction
just with our simple creation of
a Picker.
[ Applause ]
Making it trivial to build out
the rest of our three Pickers.
And just like in macOS, we still
have explicit control over the
ultimate style.
If we wanted a wheel-style
Picker here, we could again just
impose that.
Now we have a pretty nice set of
apps at this point.
But it's one thing to order
toast at our desk or while on
the go, and it's another thing
entirely to have heated debates
with friends and family about
what exactly makes the best
avocado toast.
The form on the right side
consists of the same content
that we saw in the other apps
and taking a look at the code
that's used to build it, it's
not a surprise that it's using
the same structure and control
creation that we used before.
And again, the difference is
that automatic adaptation.
For instance, Toggle being
represented using on/off buttons
instead of switches.
And this gets to the heart of
something really important
across all of SwiftUI.
The idea that you can learn a
concept once and apply it
anywhere.
SwiftUI is not just a means to
write once and run anywhere, but
it's a framework that enables
you to learn these core concepts
and use them in a variety of
different contexts and
platforms.
This scales from the modifiers
and ViewBuilder syntax to the
shared core types like color,
image and ForEach, to even these
higher-level controls.
One example that really
illustrates to me this reuse of
knowledge is a slightly
platform-specific example of
building a contextMenu.
The contextMenu itself can be
attached to an associated view
using a modifier.
And this modifier uses the
ViewBuilder syntax to define its
menu contents.
Now if we take a look at the
menu, we can see a few familiar
concepts.
Some elements that on click
perform an action and have a
label describing that action,
and others that specifically get
turned on and off.
So it's not a surprise that the
contents themselves are built up
using the same controls we've
already learned how to use.
buttons, dividers and Toggles.
But still, automatically taking
on the expected look and feel
for our macOS menu, from the
hover and accelerated gesture
handling, to the special
highlight and selection styling.
From these few examples, you can
already tell that controls in
SwiftUI are a little bit
special.
They're defined based on their
purpose, the role that they
serve, their connection to your
app's model, rather than
specifically to their visual
appearance.
And this means that they're
inherently reusable across a
variety of historic contexts,
and the appropriate look and
feel can be determined based on
that context, platform or other
information.
And at the same time, they're
customizable, both in their use
of views as labels and options
as well as being able to
arbitrarily style these controls
from the system styles like you
saw with Picker to even
completely custom-built styles.
And no matter what the style,
still having accessibility
support built right in.
Now earlier Matt showed a few
examples of using modifiers to
impose additional behavior on
views.
And the same is true for
controls as well.
One example that those of you on
iOS will already be familiar
with is changing the tint or
accent color for your UI, which
affects how many different
system controls appear.
And if we want to apply this to
our entire app, we can apply the
accentColor modifier to our
outermost view and it will be
inherited by the entire
hierarchy such as this button.
Now when it comes to disabling
controls, we can use the
disabled modifier.
For instance, disabling the
Order button when maybe there
are no toasts being ordered.
But there also might be
scenarios when we need to
disable entire groups of
controls.
For instance, when we're unable
to connect to the toast network
to even place our order, we
probably want to disable each
and every control in our form.
But this looks a little tedious
and error-prone if we ever add
additional controls.
But like you saw with modifiers
in general, we can instead lift
this modifier up and apply the
modifier to our entire form,
just like we did with the
accentColor modifier.
[ Applause ]
Now all the controls in our form
will be disabled based on this
single statement.
And all of this adaptivity and
inherited behavior is pretty
powerful and potentially comes
as a surprise since we're using
these simple value-type views.
But let's take a little look
under the hood for how some of
this works.
These examples are built on top
of something called the
environment.
And the environment consists of
all the context for where your
views appear in.
These are things that you might
have previously thought of as
being shared global state, part
of our trait collection or
properties on your view, or
maybe even had to reach up to
some ancestor object to pull the
value out.
But now this is all packaged up
into the environment.
And it's accessible to any of
you that might want to access
it.
And each view inherits that
environment from its parent.
Now as an example, when running
in an Arabic locale, the
environment at the root of our
app has a right-to-left layout
direction.
And every view inherits that
layout direction.
But at any given point, the
environment can also be
overridden for a subtree of
views.
So if we were building up some
media playback controls, we'd
want to ensure that they're laid
out left-to-right.
And so by using the environment
modifier, we can impose that on
that hierarchy.
Now the environment is also one
of the important technologies
that helps make previews so
powerful.
It enables showing the same
exact UI in a variety of these
different contexts so we can
really preview our app against
all the ways people might be
using them.
Now you've seen how the
environment automatically
affects various system views,
and custom views are able to use
the environment as well.
So I've been working on a little
control for our next update,
which allows deciding exactly
where on top of our toast an egg
should be placed.
You can see it's built up using
a simple ZStack of two images: a
toast on the bottom and an image
being positioned with a
dragGesture on top.
With that, we can tap and drag
the egg into just the right
spot.
Now if we go to use our Egg
View, there may be some cases we
need to disable it.
Maybe the shop ran out of eggs.
But since we're using a system
dragGesture, it will
automatically be disabled by the
disabled modifier.
So if somebody comes in and
tries to drag that egg, it won't
budge.
Of course, we should also offer
some visual feedback that it's
disabled as well, and thankfully
that's pretty easy.
We can add an environment
property that's connected to the
isEnabled value from the
environment.
And we can use its value just
like any other property.
For instance, reducing the
saturation of our overall
construction when it's disabled.
And if the egg placement view
ever becomes no longer disabled,
SwiftUI will automatically
recall our view's body and
re-render it to the now
undisabled state.
And again, this is SwiftUI
automatically managing our
dependencies on the environment
so we can just express our
view's relationship to it and
not have to worry about
observing for when things
change.
Now we've covered a number of
controls and how to compose
those all together.
But we're still missing one
really important piece of every
app, and that's navigating
between these screens, from the
order form to the egg placement
Picker to the order history.
Now let's start in with the
order form.
Now a problem that some of you
might have already noticed is
the look of the title in the
form.
It doesn't use the standard
navigation bar styling.
So we can first wrap our
Orderform in a NavigationView as
the content of our app.
NavigationView provides the
ability to navigate through
screens of our app revealing
more nested or detailed
information.
On iOS, NavigationView also adds
in the standard navigation bar
Chrome.
And then we can use the
NavigationBarTitle modifier to
produce that large beautiful
title for our form.
Now this modifier is a little
bit special.
It provides information that's
able to be interpreted by a
NavigationView ancestor.
We saw earlier examples of
modifiers that have information
flow down the view hierarchy
using the environment, and this
is an example of one that flows
information upwards using
something called preferences.
Now we're not going to go into
too much detail on that, but
you'll see other similar
examples later.
So focusing on the form, the
next thing we want to do is add
support for including an egg in
our order.
So we can add a little Toggle
here and then whenever somebody
opts into including an egg, we
can add a navigation row which
takes us to our
EggLocationPicker.
So let's expand out the form to
see how this works.
It's built using a Toggle bound
to whether or not our order
includes an egg.
And then it uses the same
ViewBuilder conditional that
Matt showed us earlier to
optionally include that
navigation row.
Now the really cool thing is
that we provided an animated
binding to the Toggle.
So whenever somebody taps that
switch, our navigation row will
be animatedly inserted in for
the formList just with the
setup.
And expressing the navigation
row is also amazingly simple.
It's using a specialized control
called a Navigationbutton which
allows us to provide some
destination content to navigate
to when interacted.
Navigationbutton automatically
comes with all of the right look
and feel such as the disclosure
indicator on the trailing edge.
Now because views are
lightweight, we don't have to
worry about having created the
EggLocationPicker here.
SwiftUI takes care to only
render these views once they're
actually presented.
Now inside the EggLocationPicker
we can use our PlacementView,
customize the navigation bar so
that once it's presented, the
title reflects its current
state.
We could also add a trailing
BarItem to quickly reset the egg
back to its start state.
Like you hopefully expect at
this point, the items here are
the same views we've already
learned how to use, so we can
just provide a button.
And that's all it takes to
create this complete navigation
experience.
Now we can turn our attention to
the OrderHistory.
Now we want to navigate to this,
but it isn't more detailed or
nested information of the form,
but it's instead an entirely
different section of our app.
This is more appropriate for the
use of a TabbedView.
As such, we can wrap our form in
a TabbedView just like we did
NavigationView and then add the
OrderHistory as another child.
Both have tabItemLabel modifiers
that it described to the
TabbedView how to label them in
the TabBar.
Now we can quickly jump over to
our OrderHistory.
But at this point we've a pretty
simple level of detail for the
OrderHistory and we might want
to expand this into a much more
detailed set of information that
we navigate to from our history
list.
This is another case of nesting
or showing more detailed
information like we saw earlier
with NavigationView and button.
So we can replace the contents
of our OrderHistory list so
instead of it being in list with
the OrderDetail displayed
inline, we can instead use this
new OrderDetail as the
destination for our
NavigationButtons.
And really it's this simple to
build a data-driven list that's
able to navigate to additional
content.
This works great on the iPhone
but if we take a look at the
iPad, we want this to be set up
using a master detail with a
SplitView.
Unlike NavigationStacks on
iPhone that push onto a single
RootView, here we know we have
two points of navigation: the
Master which is able to push
content onto the Detail.
So while our NavigationView
behaved correctly with just the
single RootContent on iPhone, we
want to indicate that it
intrinsically has these two
pieces of content: the
OrderHistory Master and the
DetailView.
Here we can use an
OrderDetailPlaceholder View to
act as the placeholder for when
nothing is selected.
Now with this, when a
Navigationbutton is interacted
with in the OrderHistory, it
will automatically get pushed
onto the OrderDetail.
This will behave as we expect on
the iPad and other wide-size
classes using a SplitView.
And for narrow-size classes,
will automatically collapse into
a single NavigationStack.
And of course, this works on
macOS as well, resulting in a
SplitView there.
And this isn't really write once
and run anywhere; there are
still these additional design
considerations such as the
increased information density on
macOS.
But SwiftUI is automatically
taking care of a base level of
platform look and feel from how
the SplitView behaves to the
height of the table rows, et
cetera.
So that we can learn how to use
these different concepts once
and then apply them anywhere.
And then we can focus our time
on those exciting and custom
features that make each of your
apps great.
Now we've covered a reasonable
amount of breadth in this last
hour and there are a number of
other talks that go into a lot
more detail.
We showed how state and bindings
will change how you interact
with controls, but data flow in
SwiftUI will make you rethink
altogether about data-driven UI
updates.
We built up a few custom views
using layout adjusters, but
Custom Controls in SwiftUI will
go into a deep dive on advanced
used of layout, graphics and
animations and has the most
awesome demo.
We know that many of you are
going to be eager to jump into
SwiftUI right away and might be
wondering if you can integrate
this into your existing app.
And the good news is yes,
SwiftUI is designed to be
integrated seamlessly alongside
your existing views and models.
And we have an entire talk
showing you how to do that.
We touched upon how SwiftUI is
designed to make your app
accessible to everyone out of
the box.
Of course, there will always be
some additional considerations
and this talk will go into
additional detail.
And finally, last but certainly
not least, we've shown how
SwiftUI raises the bar for how
much you can share across
platforms.
SwiftUI on all devices takes
that as a baseline and goes into
additional detail on how you can
make a great app on any
platform.
There are a few additional talks
such as WatchOS Specifics for
more details on what's driving
this and What's New in Swift.
And finally, thank all of you
for watching.
We are so excited.
[ Applause ]