SwiftUI is designed to integrate with your existing code base on any of Apple's platforms. Learn how to adopt SwiftUI on any Apple platform by adding SwiftUI views into your app's hierarchy, leveraging your existing data model and more.
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.
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.
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.
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.
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.
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.
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.
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.