Categories
Explainer Technical

Communicating state changes between UIKit and SwiftUI

It’s possible, and often necessary, to combine Apple’s legacy UI frameworks with their hottest new technology, SwiftUI. SwiftUI solves some of the most common problems Apple platform developers encounter, but it’s still a 1.0 technology. In the cases where does SwiftUI offer analogous components to UIKit or AppKit, it doesn’t always provide identical configuration options. Moreover, not all existing components have SwiftUI representations.

While the techniques for embedding SwiftUI and legacy view frameworks in one another are straightforward, one head-scratcher for new SwiftUI developers is how to make these views communicate. The old UI paradigm made every single view element its own object. To communicate between objects, you called the method you wanted and that was that.

SwiftUI, on the other hand, computes a view based on state data. Each view is a struct that describes things to be drawn on the screen, rather than an instance of an object. There’s nothing to call methods on.

Nonetheless, it’s possible to bring harmony to these two approaches in a way that is seamless to the end user, and straightforward enough to maintain for your team.

SwiftUI to UIKit

It’s a simple matter for SwiftUI to interact with UIKit objects. You can:

  • Use a closure passed in on initialization of the SwiftUI view
  • Send notifications
  • Store a reference to a UIKit object, and call methods or modify properties on it as needed—perhaps when a user taps a button

Here’s an example. We initialize this view by passing in a reference to an existing UISwitch instance:

SwiftUISwitchToggle(externalSwitch: aSwitch)

In the implementation, a button’s action modify’s the switch’s isOn property, just like the good ol’ days.

struct SwiftUISwitchToggle: View {
    
    let externalSwitch: UISwitch
    
    var body: some View {
        Button(action: {
            self.externalSwitch.isOn.toggle()
        }) {
            Text("Toggle Switch")
        }
    }
}

The result: clicking that button changes the state of the switch.

UIKit to SwiftUI

Communicating from UIKit into SwiftUI, on the other hand, is a bit more subtle. Again, SwiftUI views are structs that compute a view based on state.

So if you want to communicate with a SwiftUI view, you need to interact with that state.

One way to do this is by using Combine’s @Published property wrapper. Any variable that is published this way will send updates to its subscribers when its value changes.

Make your SwiftUI view such a subscriber and it will redraw itself when it receives those updates.

So imagine a UIViewController subclass with the following:

    @Published var switchIsOn = true
    
    @IBAction func handleSwitch(_ sender: UISwitch) {
        self.switchIsOn = sender.isOn
    }

There’s a published property called switchIsOn, and an action that changes its value. This is all you need to communicate with SwiftUI.

From here, a SwiftUI view can consume notifications from that published property. You can then adjust your view according to any changes in its value, like this:

struct SwiftUILightbulb: View {
    
    @ObservedObject var viewController: UIKitToSwiftUIViewController
    
    var body: some View {
        ZStack {
            Color(viewController.switchIsOn ? .white : .black)
            Image(systemName: viewController.switchIsOn ?  "lightbulb" : "lightbulb.fill")
                .shadow(color: viewController.switchIsOn ? .yellow : .clear, radius: 8, x: 0, y: 0)
                .foregroundColor(viewController.switchIsOn ? .yellow : .gray)
                .font(.system(size: 30))
        }.animation(.easeInOut)
    }
}

Which looks like this:

Here’s a demo project you can play with to see it in action:

https://github.com/daniloc/SwiftUI-UKitBasics

Recap

  • SwiftUI and legacy Apple UI frameworks use very different paradigms
  • SwiftUI can call out to UIKit objects in ways that feel familiar
  • UIKit objects can be subclassed to become a source of SwiftUI state, and thus trigger updates in SwiftUI views

Have fun!