WWDC 19
Integrating SwiftUI
(*) – marks APIs that were changed since the video was published
Embedding SwiftUI views in UIKit/AppKit
A SwiftUI view can be added to an AppKit or UIKit app by wrapping it in an NSHostingController
or UIHostingController
:
let viewController = UIHostingController(rootView: MyView())
You can also embed a SwiftUI view in a single NSView
or UIView
using NSHostingView/UIHostingView
:
let view = NSHostingView(rootView: MyView())
Embedding SwiftUI views in WatchKit
On watchOS, create a custom subclass of WKHostingController
first:
class MyHostingController: WKHostingController<MyView> { override var body: MyView { return MyView() } }
Then assign that class name to an interface controller on the storyboard
Call setNeedsBodyUpdate()
and updateBodyIfNeeded()
on the hosting controller to trigger a reload of the embedded view when some data changes
You can also use SwiftUI for notification content in a similar way, using a subclass of WKUserNotificationHostingController
:
class MyNotificationController: WKUserNotificationHostingController<MyNotificationView> { var notification: UNNotification? override var body: MyNotificationView { return MyNotificationView(notification: notification!) } override func didReceive(_ notification: UNNotification) { self.notification = notification } }
Embedding existing views into SwiftUI
To put existing views from the older frameworks inside a SwiftUI view hierarchy, use one of the Representable protocols: NSViewRepresentable
, UIViewRepresentable
, WKInterfaceObjectRepresentable
View controllers can be wrapped with: NSViewControllerRepresentable
, UIViewControllerRepresentable
Each of these has a set of three lifecycle methods:
- Make View/Controller (context:) – creates the view or controller object
- Update View/Controller (context:) – called once at first after "make…" and then whenever view needs to be redrawn
- Dismantle View/Controller (coordinator:) – optionally called when removing the view
The context
is an object of one of the classes named NS/UI/WK...RepresentableContext
depending on the framework
It can be used to coordinate between the old framework world and the SwiftUI world
The context has 3 properties:
coordinator
– can be used to implement patterns like delegates, data sources, target/action etc.environment
– the SwiftUIEnvironment
transaction
– tells you if there was an animation
The coordinator
is an object of any class that you create yourself that is then passed to any subsequent calls
To use a coordinator, implement the optional makeCoordinator
method and create the coordinator object there
In the "make view/controller" method you can then access the coordinator through the context and e.g. make it a delegate of a UIKit view that you create there
Integrating with the data model
To integrate SwiftUI with your data model, implement the ObservableObject
(*) protocol, which requires one property – an objectWillChange
(*) publisher that performs a send before every change in the data
Then, in the SwiftUI view add a property of the model type and mark it with @ObservedObject
(*)
The SwiftUI view will then observe the model and will update itself whenever the model notifies it of a change in the data
Your data model + state should be the only source of truth in your app
Example publishers:
NotificationCenter.default.publisher(for: notificationName, object: self)
UserDefaults.standard.publisher(for: \.someSetting)
SomeNSObject.publisher(for: \.myKeyPath)
Publishers.Merge(publisher1, publisher2)
PassthroughSubject<Void, Never>()
– just call send()
when you need
Publishers need to publish on the main thread – use .receive(on: RunLoop.main)
See more about Combine in "Introducing Combine" and "Combine in Practice"
See more about binding to observable objects in "Data Flow in SwiftUI"
ℹ️ In SwiftUI 1.0 betas, as shown in the video, the protocol was called BindableObject
, it had a property
named didChange
(which performed a send *after* the change), and the model property was marked with @ObjectBinding
Integrating with the system
A lot of system APIs are integrated by using view modifiers that make use of NSItemProvider
objects
NSItemProviders
are a Foundation technology that can be used to move data between applications and the OS in various forms
An item provider includes a collection of Universal Type Identifiers that describe the data types that a given item can be represented as, and they provide the data in the selected form when requested
Drag & Drop:
To make a view a drag source, use .onDrag
:
func onDrag(_ data: @escaping () -> NSItemProvider) -> some View
In the closure, return an item provider that contains your data
A rendering of the given view will be used as the drag image
To make a view a drop target, use .onDrop
:
func onDrop( of supportedTypes: [String], isTargeted: Binding<Bool>?, perform action: @escaping ([NSItemProvider, CGPoint]) -> Bool ) -> some View
This version passes an array of NSItemProviders
with dragged data and the drop coordinates to the closure
There is also another version that takes a delegate, which allows you more fine-grained control over the drop process:
func onDrop(of supportedTypes: [String], delegate: DropDelegate) -> some View
Pasteboard:
To accept a paste command, use the .onPaste
modifier:
func onPaste( of supportedTypes: [String], perform action: @escaping ([NSItemProvider]) -> Void ) -> some View func onPaste<Payload>( of supportedTypes: [String], validator: @escaping ([NSItemProvider]) -> Payload?, perform action: @escaping ([NSItemProvider]) -> Void ) -> some View
Unlike drag & drop, pasting is more indirect – the user does not paste the content into a specific position of the view, but simply into the view as a whole
To determine which specific control in the view hierarchy the content should go to, you need to use the focus system
Focus system:
Used on all systems, especially macOS and tvOS, but also on iOS and watchOS
Determines where to send digital crown events, where keyboard shortcuts or pasted content is sent, etc.
When an action like digital crown gesture is performed, SwiftUI checks which view is in focus, and if it has an action bound for this specific event; if not, it walks up the hierarchy to check if its superviews are interested
SwiftUI mostly handles this automatically, the only thing you need to do is to tell it which views are focusable
"Leaf" views like text fields are focusable by default, most other views aren't
To make a view focusable, use the .focusable
modifier:
.focusable(true)
You can also optionally pass a closure to be executed when the view gains or loses focus in order to change how it looks at this point:
.focusable(true, onFocusChange: { ... })
Reacting to commands:
There are some standard system commands, e.g.: (*)
.onExitCommand { ... } // menu button on tvOS, Esc on macOS .onPlayPauseCommand { ... } // play/pause button on tvOS
ℹ️ These were previously called .onExit
and .onPlayPause
, respectively
Generic command: (*)
.onCommand(#selector(Object.mySelector(_:))) { ... }
This can be used to integrate with ObjC-style actions that target the first responder, e.g. AppKit toolbar buttons, menu bar commands
ℹ️ Previously this modifier accepted a Command
object in which you had to wrap the #selector
Undo & redo:
SwiftUI uses the same Undo Manager as AppKit/UIKit
A lot of the time you may not need to change anything to use it
If you do need to access the manager directly, you can get it from the Environment:
@Environment(\.undoManager) var undoManager
Integrating with ObjC
All the standard rules of integrating Swift code with ObjC code still apply here
You can't use SwiftUI views directly from ObjC, but you can wrap them in a *HostingController
and expose that view controller to ObjC using the @objc
attribute