UIViewRepresentable: Showing UIKit components in SwiftUI

SwiftUI was unveiled to the world at WWDC in 2019. Since its introduction, Apple has offered a way for SwiftUI to bring in UIKit components. How does that work? And how can we build solutions to communicate from UIKit to SwiftUI and vice versa? This multi-part series will look into answers to those questions one piece at a time.

Our Example

Imagine we have a third party SDK that provides their functionality in a pre-packaged UIView subclass. 1 We’ll call this ThirdPartyUIView. It has properties set on it to change its behavior, methods that can be called on it, and provides internal feedback by means of a delegate object. Let’s use this as our very basic example:

class ThirdPartyUIView: UIView {
    var shouldAdd: Bool
    var delegate: ThirdPartyViewDelegate?
    
    // Same as button tap in view. Results reported via delegate
    func changeInternalValue() { /*...*/ }
}

protocol ThirdPartyViewDelegate {
    func view(_ view: ThirdPartyUIView, changedValueTo newValue: Int)
}

As you can see, our example ThirdPartyUIView has a boolean that can change internal behavior to either add or subtract a value, as well as a delegate that is notified whenever this internal state (we’re using an Int as an example) is changed. Lastly, we’ll assume this view has a button that changes this internal value, and the action performed by that button is exposed for us to call programmatically.

With this defined, let’s explore how we can work with this view and all of its functionality in SwiftUI.

Displaying a UIView

First, how do we get a UIView to appear in SwiftUI? This is where UIViewRepresentable can help us. 2 We declare a struct that conforms to this type. Its purpose is to provide the underlying UIView type that we want to wrap and display, as well as functions for creating and updating said view.

struct ThirdPartyViewRepresentable: UIViewRepresentable {
    func makeUIView(context: Context) -> ThirdPartyUIView {
        let view = ThirdPartyUIView()
        
        return view
    }
    
    func updateUIView(_ uiView: ThirdPartyUIView, context: Context) {
        
    }
}

With the code above, we can now use our ThirdPartyUIView in a SwiftUI hierarchy. For example:

struct ThirdPartyDemoView: View {
    var body: some View {
        ThirdPartyViewRepresentable()
    }
}

If we loaded this view, we would see our ThirdPartyUIView within our SwiftUI application. However, because we haven’t exposed any functionality, we can’t really do anything with the view yet. Let’s change that.

Updating View Properties

Let’s start with changing properties on the underlying view. As you’ll recall above, our ThirdPartyUIView includes a shouldAdd property. If true, each action performed by the view will add one to the internal value; false will have it subtract instead.

First, we want to allow this option to be set when we create the view. To do so, we need to provide it when creating our view representable.

struct ThirdPartyViewRepresentable: UIViewRepresentable {
    var shouldAdd: Bool
    
    func makeUIView(context: Context) -> ThirdPartyUIView {
        let view = ThirdPartyUIView()
        view.shouldAdd = shouldAdd 
        return view
    }
    
    func updateUIView(_ uiView: ThirdPartyUIView, context: Context) {
        
    }
}

Now, when we create our view, we can provide a value that will be used when initializing the view. As this is something we might want to change as we use it, we’ll add it as a @State property on our view along with a Toggle that will let us change it as needed.

struct ThirdPartyDemoView: View {
    @State private var shouldAdd = true
    
    var body: some View {
        ThirdPartyViewRepresentable(shouldAdd: shouldAdd)
        Toggle("Add?", isOn: $shouldAdd)
    }
}

However, we’ve now got an issue. If you change the value of the Toggle, behavior of the view doesn’t change! Why?

When the value of shouldAdd changes, the underlying view is not recreated, so makeUIView(context:) is not called again. Rather, the view is updated. Thus, we need to update the property inside of the updateUIView(_:context:) method. 3

struct ThirdPartyViewRepresentable: UIViewRepresentable {
    var shouldAdd: Bool
    
    func makeUIView(context: Context) -> ThirdPartyUIView {
        let view = ThirdPartyUIView()
        view.shouldAdd = shouldAdd
        return view
    }
    
    func updateUIView(_ uiView: ThirdPartyUIView, context: Context) {
        uiView.shouldAdd = shouldAdd
    }
}

With the above addition, when we toggle the state of shouldAdd, the view now updates and behaves as expected.

Now that we can change properties on the underlying UIView, let’s look at how we can expose the ThirdPartyViewDelegate.


  1. While this series of posts will explore exposing and working with a third party’s UIView in our own SwiftUI views, these tips can also apply to taking your own existing UIKit code and using it in SwiftUI yourself. ↩︎
  2. When working with UIKit, we also have UIViewControllerRepresentable. On the Mac with AppKit, NSViewControllerRepresentable and NSViewRepresentable exist as counterparts. This post talks exclusively about UIViewRepresentable but the principles will apply to the other representable options as well. ↩︎
  3. While we’re working with a single property, there can be some gotchas when dealing with multiple properties, or when trying to update parts of a view that make cause state changes. Chris Eidhof has a great post with some examples of these gotchas. ↩︎

Discover more from Josh Hrach

Subscribe now to keep reading and get access to the full archive.

Continue reading