MacKuba

Kuba Suder's blog on Mac & iOS development

Integrating SwiftUI

Categories: SwiftUI, WWDC 19 0 comments Watch the video

(*) 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 SwiftUI Environment
  • 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)