MacKuba

🍎 Kuba Suder's blog on Mac & iOS development

WWDC 19

Integrating SwiftUI

Categories: SwiftUI 0 comments Watch the video

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



Leave a comment

*

*
This will only be used to display your Gravatar image.

*

What's the name of the base class of all AppKit and UIKit classes?

*