Integrating SwiftUI
(*) marks APIs that were changed since the video was published
Embedding SwiftUI views
A SwiftUI view can be put inside a native framework view by wrapping it in a NS/UI/WKHostingController
:
UIHostingController(rootView: MyView())
You can also embed a SwiftUI view in a single UIView
or NSView
using UI/NSHostingView
:
NSHostingView(rootView: MyView())
On watchOS, use a subclass of WKHostingController
:
class MyHostingController: WKHostingController<MyView> { override var body: MyView { return MyView() } }
Call setNeedsBodyUpdate()
or updateBodyIfNeeded()
to trigger a reload when some data changes
You can also use SwiftUI for notification content in a similar way, with 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
For putting existing views inside a SwiftUI view hierarchy, use Representable protocols: NSViewRepresentable
, UIViewRepresentable
, WKInterfaceObjectRepresentable
View controller can be wrapped with: NSViewControllerRepresentable
, UIViewControllerRepresentable
Lifecycle methods:
- Make View/Controller (context:) – building the view or controller object
- Update View/Controller (context:) – called once at first and then whenever view needs to be redrawn
- Dismantle View/Controller (coordinator:) – optionally called when removing the view
context = NSView[Controller]RepresentableContext
/ UIView[Controller]RepresentableContext
/ WKInterfaceObjectRepresentableContext
Context has 3 properties:
coordinator
– can be used to implement patterns like delegates, data sources, target/action between the two “worlds”environment
– the SwiftUIEnvironment
transaction
– tells you if there was an animation
To have access to the coordinator, implement optional makeCoordinator
method and build a coordinator (which is just any custom object), then in makeView()
make the coordinator (pulled out from the context) a delegate of the UIKit view
Integrating with the data model
To integrate SwiftUI with your data model, use @ObservedObject
(*) and ObservableObject
(*) protocol, which includes an objectWillChange
(*) publisher
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)
Integrating with the system
A lot of system APIs are integrated by using view modifiers that make use of NSItemProvider
objects:
Drag & Drop:
func onDrag(_ data: @escaping () -> NSItemProvider) -> some View func onDrop( of supportedTypes: [String], isTargeted: Binding<Bool>?, perform action: @escaping ([NSItemProvider, CGPoint]) -> Bool ) -> some View func onDrop(of supportedTypes: [String], delegate: DropDelegate) -> some View
Pasteboard:
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
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
Custom views aren't focusable by default (unlike text fields etc.) – to make a view focusable, use the modifier:
.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
Generic command: (*)
.onCommand(#selector(Object.mySelector(_:)
)) { ... }
This can be used to integrate with e.g. AppKit toolbar buttons, menu bar commands
Undo & redo:
Access UndoManager
through @Environment(\.undoManager)