Transcript
[ Music ]
[ Applause ]
>> Good afternoon.
My name is Tanu Singhal.
I'm here with my colleague
Raleigh, and today we'll talk
about Integrating SwiftUI.
Our first goal today is to help
you add SwiftUI in your existing
apps.
Next, we'll learn to embed
UIKit, AppKit and WatchKit views
in SwiftUI.
Later, we'll talk about
strategies for setting up your
data model so that works well
with SwiftUI.
And finally, we'll learn to use
drag and drop, the paste mode,
and the focus system in SwiftUI
so our apps integrate better
with the entire system.
It is incredibly easy to host
SwiftUI content in your apps.
We do this with the help of
hosting controllers.
A hosting controller can be used
to set your SwiftUI views as the
content of a ViewController or
InterfaceController.
Hosting controllers are
available on all three
frameworks.
Let's take a closer look at the
UIHostingController first.
The UIHostingController is a
subclass of UIViewController.
It has an initializer that takes
a single parameter called
rootView.
This is where we pass our
SwiftUI view.
Once the hosting controller is
initialized, it can be presented
via code or storyboard just like
any other view controller.
For Mac, we have the
NSHostingController which is
used to present SwiftUI content
in an NSViewController.
Now, if you're a Mac developer,
you may not always want to
present an entire view
controller.
Perhaps, you want to embed a
small SwiftUI view in your
existing AppKit view hierarchy.
For this, we have the
NSHostingView.
The NSHostingView is a subclass
of NSView and it can be used to
present SwiftUI views directly
in AppKit view hierarchies.
If you're using Auto Layout, we
automatically respect the
content size preferences of your
SwiftUI views and your layout
will look as you expect.
Auto Layout also works well when
using container views to embed
UIHostingControllers or
NSHostingControllers in your
existing AppKit or UIKit view
hierarchies.
For watchOS developers, we have
the WKHostingController.
To use this, you first create a
subclass, and in the subclass.
we return the SwiftUI view from
the body.
Next, go to the storyboard and
select any interface controller
or hosting controller.
From the identity inspector, set
the class as your custom
subclass for the
WKHostingController.
Now, this hosting controller can
be used like any other interface
controller.
If you need to invalidate the
body due to a change occurring
on the WatchKit side, you can do
that by using the
setNeedsBodyUpdate and the
updateBodyIfNeeded methods.
It is also easy to use SwiftUI
content in dynamic interactive
notifications.
For this, we use the
WKUserNotification
HostingController, and again we
first return to SwiftUI View
from the body and this body gets
updated every time the
didReceive notification method
is called.
To learn more about building
Watch apps with SwiftUI, check
out the SwiftUI on watchOS
session.
Now, let's take a look at an
example App where we would need
to use hosting controllers.
I went to buy some plants the
other day and I learned so much
about different plants and trees
that I decided to build an app
to catalog these plants.
I started building my app using
UIKit and what you see here is a
standard UIKit table view
controller.
Then, I learned about SwiftUI
and I created a details view for
my app using SwiftUI.
Now, we haven't added any code
to navigate from my
UIKitTableViewController to
SwiftUI.
Let's go into a demo and see how
we can set this up.
Here, we have our SwiftUI view
which is used to present our
details view.
And this is the view we want to
navigate to when we tap in our
table.
So let's go to the storyboard
and the storyboard will go into
the Library and find a hosting
view controller.
Let's drag this in our
storyboard.
I'll select this cell in the
table, hold down the Control
key, and drag to the hosting
controller.
We'll select the Show segue.
Now, we need to add content to
this hosting controller.
So let's open our table view
controller on the side.
Let me hide some of these panels
to make more space.
I'll now select the segue we
just created, hold the Control
key and drag into the view
controller code.
This creates an IBSegueAction.
IBSegueActions are new in Xcode
11 and they allow you to connect
segues in the storyboard to your
view controller's code.
By using these, you can directly
set properties in your
destination view controllers
without having to use the
prepareForSegue method.
[ Applause ]
Thank you.
Let's close the storyboard since
we'll focus on the code.
Here, I'll create an instance of
our SwiftUI view.
The rootView here is set to our
PlantDetailsView which was our
SwiftUI view.
All we need to do is pass this
rootView to the hosting
controller's initializer.
Now when we run our app, we'll
be able to navigate from our
UIKitTableViewController to our
PlantDetailsView which is in
SwiftUI.
[ Applause ]
We saw in this demo how easy it
is to add SwiftUI to your apps.
Next, we'll learn to embed views
created with existing frameworks
inside SwiftUI views.
To do this, we use the
representable protocol.
The representable protocol
allows us to present UIViews,
NSViews, and WKInterfaceObject
in SwiftUI.
Additionally, we can also
present view controllers in
SwiftUI with the help of view
controller representable
protocols.
The representable protocol has
two required methods, the Make
Method and the Update Method.
The Make Method is where you
create the view or controller
that you want to present in
SwiftUI.
And the Update Method is where
you update this view to the
current configuration.
During initialization, the Make
Method is called first followed
by the Update Method.
The Update Method can be called
multiple times whenever an
update is requested by SwiftUI.
Finally, we offer you with an
optional Dismantle Method where
you can put any clean up code
that needs to run before your
view or controller is removed.
Let's now take a look at the
Swift definitions of these
methods.
Notice that the Make, Update and
Dismantle methods look and
behave similarly across
frameworks.
For AppKit and UIKit, these
protocols can be used to present
views and view controllers in
SwiftUI.
For WatchKit, we can use the
WKInterface representable
protocol to present a subset of
WKitInterfaceObjects in SwiftUI.
You can look up the full list of
supported WatchKitObjects on
developer.apple.com.
So as we mentioned, the Make and
Update methods are the only two
required methods that's all you
need to use simply present your
views in SwiftUI.
However, views are often complex
and you may want to do more than
simply present them.
Perhaps, you want to expose
target action or delegation in
SwiftUI, or you may want to read
from the environment of SwiftUI
and respond accordingly.
It could also be useful to
understand if there was an
animation on your view.
To enable you to create better
integration between your views
and SwiftUI, we have created the
representable context.
The representable context has
three properties.
The first is the coordinator
which helps coordinate between
your views and SwiftUI.
The coordinator can be used to
implement common patterns like
delegation, data sources, and
target action.
The next property is the
environment which will help you
read about SwiftUI's
environment.
This could be the system
environment like color scheme or
size classes or clear direction.
Or it could be app-defined
custom environment properties.
Finally, the transaction
property let's our views know
whether or not there was an
animation in SwiftUI.
The representable context is
available for views, view
controllers as well as interface
controllers.
Now, let's take another look at
the representable protocol
diagram that we saw.
In the Make and Update methods,
we passed a parameter for the
representable contexts.
This context already has
information about the
environment and transaction.
If you wish to use the
coordinator however, you would
have to create that yourself.
That can be done using the
optional Make Coordinator
method.
During initialization, the Make
Coordinator method is called
first followed by the Make View
and Update View methods so that
the coordinator is available to
the context when you're
configuring your views.
It might be easier to understand
these concepts with the help of
an example.
We'll take another look at our
plants app and here's the
details view that we saw before.
This is a view created in
SwiftUI.
I've also created another view
in UIKit.
This is to present the ratings.
What I want to do is embed my
UIKit base ratings control
inside my SwiftUI view.
In addition to that, I also want
to add a label in SwiftUI that
can read the rating from my
ratings control.
We can do all this with the help
of a UIViewRepresentable
protocol.
Let's go into a demo and set
this up.
In this project, I've included a
UIKitRatingsControl which is my
UIKit base view that renders
those five stars.
It is this view that we want to
present in SwiftUI.
The RatingsControlRepresentation
is simply a wrapper around our
UIKit view and it will enable us
to present our UI view in
SwiftUI.
We've also added the two
required methods for the
UIViewRepresentable protocol,
makeUIView and updateUIView.
In the makeUIView method, all I
need to do is create an instance
of my UI view which is the
UIKitRatingsControl here and
return it.
And with this code, I can start
using the
RatingsControlRepresentation
inside SwiftUI.
Let's take a look at the
preview.
These stars are being displayed
by calling the
RatingsControlRepresentation in
SwiftUI which in turn presents
my UIKit base view.
Notice, however, that all the
stars are grayed out.
To set the highlighting
correctly based on the rating,
we would need to read this
rating form SwiftUI and set it
on our UIKit view.
In our code, I've already added
a binding for the rating so we
can read this from SwiftUI.
Now, let's go into our
updateUIView method and set the
rating on the UI view.
Immediately, our stars highlight
as we expect and we're reading
this rating from SwiftUI.
[ Applause ]
Thank you.
We'll go into the live mode for
the last preview.
In this preview, I've included a
Clear button in SwiftUI which
updates the rating to zero.
Notice when I tap this, our
UpdateUIView gets-- method gets
called and the rating gets
updated on our UI view.
We can also tap on the stars to
change the rating.
When I tap on these stars
though, the label on the right
is not getting updated properly.
The reason is that our UI view
is changing-- is intrinsic value
for the rating and is not
conveying that back to SwiftUI.
To address this, you would have
to use a target action pattern.
So we'll go ahead and implement
that.
To use the target action
pattern, we need to add a
coordinator.
Let's create a coordinator here.
This is simply an NSObject where
we're storing the values that
we're interested in.
And we have an initializer to
set the values that we care
about which is just the rating
here.
Next, we've added a selector for
the rating called ratingChanged
which will get called through
our target action pattern.
And here, we simply set the
rating of the coordinator to be
the rating that we get from our
UI view.
We can now implement the
makeCoordinator method.
In this method, we simply return
an instance of the coordinator
and we're passing the rating
binding from our
RatingsControlRepresentation.
Finally, we can use this
coordinator in our makeUIView
method and add the target.
With this, our coordinators
ratingChanged method gets called
whenever the valueChanged event
is triggered in UIKit.
So we are ready to add this to
our app now.
I'll go into the
PlantDetailsView, call the
RatingsControlRepresentation
here, and we can also set a
frame that gets respected by
UIKit.
So let's resume the previews.
I'll go into the live mode.
And now when I tap on the stars,
the value and the text label
gets updated as we expect.
[ Applause ]
We hope you're excited about
using SwiftUI in your apps.
As a first step, go ahead and
create some hosting controllers
with SwiftUI content.
It's really easy to add these to
your apps using IBSegueActions.
If you've already created views
that you want to embed in your
SwiftUI view hierarchy, you can
check out the representable
protocol.
And finally, be sure to leverage
the representable context for
more advanced functionality.
Next up, Raleigh will talk about
integrating your data model with
SwiftUI.
[ Applause ]
>> Thank you, Tanu.
My name is Raleigh Ledet.
I'm an engineer on both SwiftUI
and on AppKit.
You've seen how easy it is for
you to quickly add SwiftUI views
to your existing applications.
And we're already getting data
in there.
But I want to talk about really
integrating this with your data.
What you saw on Tanu's demo is
we took our plants data model
and we passed it to our root
SwiftUI view.
And this is really great because
SwiftUI view was able to take
out the appropriate properties
and we were able to render this.
However, this was more of a
one-shot operation because our
data model is the existing
outside of our SwiftUI
framework.
And this means if there are any
changes to our data model
perhaps from the cloud or even
from the user, SwiftUI doesn't
know about it and it won't
re-render with our data content.
Our solution for this is the
BindableObject data protocol.
And it's a really simple
protocol.
All you have to do is vend one
didChange publisher.
Once you implement the didChange
property and you are conforming
to the BindableObject protocol
in your SwiftUI view where
previously we were just
referencing our data model, we
can now use the @ObjectBinding
wrapper.
This allows SwiftUI to see that
we're referencing a
BindableObject and then it knows
that it can subscribe to it for
this view.
And now whenever your data
changes, the didChange publisher
will emit to all of its
subscribers that something in
the data has changed.
And of course, one of those
subscribers is SwiftUI.
And now SwiftUI automatically
knows which views were
referencing your data model and
need to be updated.
In addition, we can use the
dollar prefix with data and we
can use a binding to our data
model.
And this would allow us to have
rewrite access directly top our
data model.
So now, whenever we make changes
perhaps in a text field, those
changes are updated directly in
our data model.
So as you can see, the
BindableObecject protocol is
very simple.
Again, just one property that
you have to implement.
And it's incredibly flexible.
We've designed it so that there
are number of publishers that
will work with whatever kind of
notification system you're using
to note changes occurring in
your data model.
And the key point here is that
we want to make sure that your
data model remains the single
source of truth for data in your
application.
And this is very important in
SwiftUI.
We always want either your data
model or any state that you have
in SwiftUI to only have a single
source of truth.
Combined with the declarative
view hierarchy, this eliminates
the need to write view
controllers to synchronize data.
Now, let's see it in a demo.
When Tanu came up to me and she
told me about her plants idea, I
was extremely excited.
And I wanted to jump onboard and
help out and I wanted to start
writing the Mac application
version.
So we started writing the
application.
She was working on the iOS
version and I was working on the
Mac version.
And we got to the same point.
And when we saw at SwiftUI come
out and we were amazed because
we were able to move so quickly
with the SwiftUI and write our
details view.
So here we are running the same
application but now on the Mac.
So here I have an NSTableView on
the left and we have the same
details view on the right.
But now we want to go a step
further and really integrate
this with our data model so that
we can make changes in our
details view and have that
reflected in our data model.
Here's our data model.
And you can see it's a really
simple data model.
All we have is an array of
plants.
Well, what I really want to
point out is that whenever the
plants change, we're issuing a
PlantsDidChange notification.
And we're doing this so that the
table view in our view
controller can listen for this
and know to reload data in table
view.
But since we're already posting
a notification, we're going to
use that to implement a
didChange BindableOBject.
So here's our implementation of
didChange.
We're using a NotificationCenter
publisher for the
PlantsDidChange.
And we're using self as the
object that we want to watch.
Now, I do want to point out one
more thing real quick, changes
to your data need to be informed
that a change was needed to be
told to SwiftUI on the main
thread.
So we're using the receive(on)
operator to make sure that our
publisher emits to all of its
subscribers on the main thread.
Now that our data model us in
place, we can move to a details
view.
And here you can see where we
were referencing our
PlantsDataModel.
Now, we can simply add our
ObjectBinding.
And now that we're using the
ObjectBinding wrapping, we're
going to go down to where we
have -- already on isEditing
state but we're currently using
an EmptyView.
But I've already written in
EditablePlantsView and what I
want to point out is here we're
using the dollar prefix so that
we can get a binding to a
specific plant index.
Now, we'll rerun our
application.
And now with that simple change,
we could -- still works as
before and when we click the
Edit button, we can now edit the
various properties.
And you can see that the table
view updated right away and, you
know, I really like the Hawaiian
hibiscus so I'm going to set it
to 5 stars.
So now we've updated the data
directly on our data model and
we could get back to it anytime.
[ Applause ]
So as you saw in that demo, I
was already using a
NotificationCenter so I just
simply used the
NotificationCenter publisher.
But we have other types of
publishers as well.
For example, key value observing
publishers.
Any object that is KVO compliant
has a key value observing
publisher.
And you can acquire it by just
using the publisher for key path
function.
But let's look at a little bit
more interesting example.
In this example, I have a class
that's watching the user default
for a couple of changes in the
user defaults.
And the interesting part is
here, we're creating a publisher
for each user default.
One for userOption1 and one for
userOption2, but we're merging
them together into a single
publisher that we apply to our
didChange property.
So now, whenever either
userOption1 changes or
userOption2 changes, our
didChange combined publisher
will emit the change at SwiftUI
and SwiftUI will go ahead and
update our views.
All these publishers come
courtesy of the combined
framework and there are many
more publishers there that you
can look at.
And the combined framework is a
great new framework that is a
unified declarative API for
processing values over time.
In addition to the publishers as
you saw with the merge operator,
there are a number of different
operators to do complex merging
or zipping together of various
publishers.
I really suggest that you watch
the Combine in Practice talk.
However, I do want to point out
one more specific publisher and
that's the PassthroughSubject.
If you run into a situation with
your data model that none of the
other publishers from the
combine framework work perfectly
for your situation, you can use
the PassthroughSubject like I
did in this core data example.
If you have a core data
application, then you're already
using an
NSFetchedResultsController to
get a slice of data out of your
database.
And the
NSFetchedResultsController wants
you to provide a delegate to
help coordinate.
And one of the delegate messages
that you'll need to implement is
the controllerDidChangeContent.
And this lets you know when data
in the database changes.
And when that happens, we need
to let SwiftUI know.
So we'll simply grab our
didChange PassthroughSubject
publisher and we'll tell it to
manually send the change.
And now SwiftUI will see that as
one of the subscribers and all
of your views will be updated.
I focused here on
BindableObject.
But there are number of tools to
help you manage data outside and
inside of SwiftUI.
The Dataflow in SwiftUI is a
great talk that goes on to all
of these data tools into more
detail and also discusses when
it is appropriate to use which
type of tool.
Now of course, integrating with
your data model isn't the only
thing you need to think about
when you're running a SwiftUI
interface.
You also need to think about
integrating with the rest of the
system, with drag and drop on
iOS and macOS, with tvOS, and
with the Digital Crown perhaps
on watchOS.
So let's start off this talk by
talking about item providers.
Item providers are a great
technology that's provided by
the foundation framework which
provides you a means of moving
data around your application in
various forms.
It's also a tool that we use to
help transfer data across
processes.
Item providers are basically a
collection of universal type
identifiers that describe the
type of data that your item can
be represented as.
And then of course you need to
provide the data in that type
upon request.
And remember, earlier on in the
demo, I made a point to say that
we needed to have data change on
the main thread, and item
providers are asynchronous.
So when you so get data form an
item provider, you'll need to
make sure that you actually
change the data of have your
publisher emit on the main
thread.
We use item providers and drag
and drop.
So you can use the onDrag
modifier to enable your view to
be a drag source.
So now when a user starts to
drag on your view, we will call
the closure and you can provide
an item provider which will
provide the data associated with
that view and we'll-- we will
automatically take a rendering
of your view and use that as the
drag image.
To accept a drop, you can use
the onDrop modifier.
With the onDrop modifier, you
simply pass an array of
universal type identifier
strings that describe what kind
of data you can accept on your
view.
And if the user drops data of
that type on your view, we will
automatically call your action
closure and provide an array of
item providers that can form to
that type.
And of course we'll also tell
you exactly the point inside
your view that the drop
occurred.
There's another variant of the
onDrop modifier, one that takes
a delegate instead of a drop
closure.
And the delegate gives you a
little bit more visibility into
the drop process as it's moving
around in your views.
For example, you can get a
cursor location before the user
actually lets go and commits to
the drop action.
We also use item providers with
the pasteboard.
So if you want to accept a paste
command for example, you can use
the onPaste command modifier.
Similar to the onDrop modifier,
you supply an array of universal
type identifiers that you can
accept and when the user paste
on your view, we will go ahead
and provide an array of item
providers.
However, I want to point out
something that really makes
onPaste different than onDrop.
The first part is that there's
no location parameter in the
closure.
And that's a key to what's
really going on here.
When you do drag and drop, user
is directly targeting via the
cursor or the touch location
which view should accept the
drop but a paste command is more
indirect.
The user is either choosing
paste from the menu or is
perhaps using a keyboard
shortcut or the great new
gestures that exist in iOS.
The way we solve the problem of
knowing which view that the
paste command should go to is
with the focus system.
And the focus system is a very
important tool that we have that
the users use to navigate the
various UI elements and inform
us where not specifically
directed actions should go.
It's very useful on the Mac of
course for keyboard input, menu
action commands, in iOS for
keyboard commands there, and
additionally, the new gestures
for copy-paste and undo and
redo.
It's extremely important on tvOS
where the focus is how we can
determine where the user wants
the Siri Remote button actions
to be performed on.
And for watchOS, we use focus to
determine where to send Digital
Crown events.
The way this works, is you have
your view hierarchy and at some
point one of your views is going
to be the focus view.
And when the user performs an
indirect action like turning the
Digital Crown, we check to see
if the focus view has a view
modifier for that kind of
command.
In this case, the Digital Crown
command.
If it does, we'll go ahead and
call the appropriate closure.
If not, we walk up the ancestors
to try and find another-- an
ancestor view that has the
Digital Crown command and call
the appropriate closure.
SwiftUI also goes ahead and
takes care of moving focus from
one view to another
appropriately on each platform.
The only thing you need to do is
let us know which of your
SwiftUI views can gain focus.
And you do that via the
focususable modifier.
With the exception of our leaf
controls like text fields, UI--
SwiftUI views do not gain focus
by default.
So you'll need to use the
focususable modifier and
Passthrough to let us know that
this view can gain focus.
You can optionally pass a
closure that we will call to let
you know when your view gains or
loses focus and you can use this
to update your UI to give visual
feedback to the user to let them
know.
We have a number of commands
like onExit and onPLayPause.
These are examples from the tvOS
when using the Siri Remote.
But the one I really want to
talk about right now is the
generic onCommand modifier.
This is what you use to direct
actions from Objective-C-style
action selectors from menus for
example that are wired to the
first responder or Toolbar
buttons that are likewise wired
to the first responder.
And again, this is how you would
also use this on iOS.
And they're chainable.
So if your view can accept three
menu items for example, you
would also have three onCommand
modifiers attached to your view
with each having the appropriate
selector.
Now, I know all of your
applications out there already
have great undo and redo
support.
And in SwiftUI, we use the same
UndoManager that you're already
using.
And it turns out that in most
cases when you add new SwiftUI
to your application, you don't
need to do anything new with the
UndoManager.
This is especially true if most
of your undo registrations are
being done at a lower level
closer to your data model.
However, if you do need access
to the UndoManager, you can get
it using the Environment
property wrapper with the
UndoManager key path.
Now SwiftUI is obviously a Swift
based API, but we know that you
already have a lot of
Objective-C code out there.
And Objective-C in Swift already
can be integrated together quite
well.
And likewise, you can use
SwiftUI with your Objective-C
code as well.
The standard Objective-C/Swift
integration rules apply, and
basically, what that means as it
concerns SwiftUI is that you'll
need to wrap your hosting
controllers or your hosting
views in Swift.
In this case, I'm going to wrap
a hosting controller and I'll
make a subclass of
UIViewController and I give it
the at Objective-C attribute.
This will allow us to later
instantiate these Swift class
from within our Objective-C
implementation file.
And now, inside of our Swift
implementation, we can
instantiate a
UIHostingController and pass it
in appropriate SwiftUI RootView.
Now that we have a
UIViewController, we can go
ahead and use it from or
Objective-C implementation files
like we would in the other
UIViewController.
In this example, we're just
presenting it.
Similarly, you'll need to wrap
your data model as well.
And we do that so that we can
implement the BindabelObject
protocol.
So here we have a simple Swift
class that we're going to
implement the BindableObject
protocol via a
NotificationCenter publisher
here.
And this is an example.
Let's just say ObjCDataModel is
already issuing a
NotificationCenter
notifications.
And then we'll just have a
reference to our ObjCDataModel.
And the way this works is, in
your wrapping-- and you
WrappedHostingController, you
would pass a pointer to your
ObjCDataModel, and in your
hosting controller, you can
create your WrappedDataModel and
assign it your ObjCDataModel and
pass your WrappedDataModel to
your SwiftUI rootView.
And with just those two simple
wrapped classes, you would now
have seamless integration with
SwiftUI and your Objective-C
code.
So as you can see, it's really
easy to start using SwiftUI in
your applications today.
As you're adding new UI, start
using SwiftUI.
We are having a lab coming up
tomorrow at 11:00 a.m. Try out
SwiftUI in your applications.
If you have any problems, come
talk to us in the lab.
We're more than happy to talk to
you.
We're excited about this but we
can't wait to see what you come
up with.
Thank you.
Enjoy the rest of the show.
[ Applause ]