Skip to content

Frustrated by missing features in SwiftUI? Using UIViewRepresentable to wrap UIKit controls

When the SwiftUI announcement came out at WWDC 2019, you were buzzing about the new framework. Finally, a new way to build our interfaces. Out with ViewControllers, out with storyboards, in with declarative, programmatic UIs!

Now that we’ve all had some time to play with the framework, we’ve found out what we’re really trading away when we use SwiftUI, at least for now. One of the major things? The vast array of components and configurability available through UIKit.

You’ve probably found through experimenting with the new framework that there are things missing. Entire UIKit components are missing, and things like the broad number of properties you could set on TextFields, and the common pull-to-refresh control are missing.

You really don’t want to continue down the UIKit road. You want to go all in on SwiftUI now; it’s the future of development for all Apple platforms. “But how am I going to get back these features?”

This is where UIViewRepresentable comes in. Apple knew that SwiftUI was not going to be a complete port of UIKit for its first version, and so they provided this protocol which you can implement on a View struct to wrap a UIKit control, making it usable in SwiftUI. Let’s see an example how to use this: getting the pull-to-refresh functionality on a scroll view.


UIViewRepresentable works very similarly to a standard SwiftUI View, but specifies additional methods to implement:

  • makeUIView(context:)
  • updateUIView(uiView:context:)
  • makeCoordinator:

The first two methods are required, and construct the UIView, and update it when the view’s state changes, respectively.

For some components, this is sufficient, if you don’t need to handle any events. If you need an object that will receive events, you will need what UIViewRepresentable calls a coordinator to receive those events. That is where makeCoordinator comes in; it constructs the object that will receive those events, which you will configure the control to use in makeUIView.

With that in mind, here is our sample UIViewRepresentable, which wraps a UIKit UIScrollView:

struct LegacyScrollView : UIViewRepresentable {
    // any data state, like @State/@Binding/etc, if needed

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIView(context: Context) -> UIScrollView {
        let control = UIScrollView()
        control.refreshControl = UIRefreshControl()
        control.refreshControl?.addTarget(context.coordinator, action:
            #selector(Coordinator.handleRefreshControl),
                                          for: .valueChanged)

        // Simply to give some content to see in the app
        let label = UILabel(frame: CGRect(x: 0, y: 0, width: 200, height: 30))
        label.text = "Scroll View Content"
        control.addSubview(label)

        return control
    }


    func updateUIView(_ uiView: UIScrollView, context: Context) {
        // code to update scroll view from view state, if needed
    }

    class Coordinator: NSObject {
        var control: LegacyScrollView

        init(_ control: LegacyScrollView) {
            self.control = control
        }

        @objc func handleRefreshControl(sender: UIRefreshControl) {
            // handle the refresh event

            sender.endRefreshing()
        }
    }
}

makeUIView is where the UIScrollView is constructed with the UIRefreshControl. In this example, there is a hardcoded UILabel, but you can put any children for the UIScrollView in here. (Aside: You could even make a constructor for LegacyScrollView which would take SwiftUI Views, and wrap them in a UIHostingController, allowing you to use SwiftUI Views in your wrapped UIKit view, but that is beyond the scope of this article.)

Note that makeUIView makes use of the context argument passed in. The context argument contains a few properties, but the most important one is the coordinator, which SwiftUI set earlier by calling the makeCoordinator function on our class. So now we can set the handleRefreshControl to handle the refresh control when it is pulled down, triggering the refresh logic, and then ending the refresh cycle.

If this control had any state variables, then updateUIView would be the place to update the UIScrollView to reflect them on the screen. updateUIView should get called anytime the state variables on your control are changed, just like the SwiftUI View body property.

SwiftUI is going to be growing for a long time, slowly integrating pieces of UIKit functionality over that time. But if you’re ready to use SwiftUI now, but still want some of that missing functionality from UIKit, UIViewRepresentables are going to be your way to get both with (relatively) little fuss.

Comments are closed.