Integrating UIKit with SwiftUI using UIViewRepresentable and UIViewControllerRepresentable

SwiftUI and UIKit are both powerful frameworks for building user interfaces in iOS applications. While SwiftUI offers a modern, declarative approach, UIKit has been the foundation of iOS development for many years and has a vast collection of pre-built components and legacy code. Integrating these two frameworks can be highly beneficial, especially when migrating existing UIKit-based projects to SwiftUI or when you need to use UIKit components that are not yet available in SwiftUI.

Why Integrate UIKit and SwiftUI?

  • Leverage Existing UIKit Code: Re-use existing custom UIKit views and controllers in SwiftUI projects.
  • Gradual Migration: Migrate parts of your app to SwiftUI while keeping other parts in UIKit.
  • Access UIKit-Specific Features: Use UIKit features that are not yet available in SwiftUI.

How to Integrate UIKit with SwiftUI

SwiftUI provides two protocols to facilitate the integration of UIKit elements: UIViewRepresentable and UIViewControllerRepresentable.

1. Using UIViewRepresentable

UIViewRepresentable is used to wrap a UIView (a basic building block for UI in UIKit) so it can be used in SwiftUI.

Step 1: Create a UIKit View

Let’s start by creating a simple custom UIView.


import UIKit

class CustomUIView: UIView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupView()
    }

    private func setupView() {
        backgroundColor = .systemBlue

        let label = UILabel()
        label.text = "Custom UIKit View"
        label.textColor = .white
        label.textAlignment = .center
        label.translatesAutoresizingMaskIntoConstraints = false
        addSubview(label)

        NSLayoutConstraint.activate([
            label.centerXAnchor.constraint(equalTo: centerXAnchor),
            label.centerYAnchor.constraint(equalTo: centerYAnchor)
        ])
    }
}

In this example, CustomUIView is a simple view with a blue background and a centered label.

Step 2: Conform to UIViewRepresentable

To use the CustomUIView in SwiftUI, create a struct that conforms to UIViewRepresentable.


import SwiftUI

struct CustomUIViewRepresentable: UIViewRepresentable {
    func makeUIView(context: Context) -> CustomUIView {
        return CustomUIView()
    }

    func updateUIView(_ uiView: CustomUIView, context: Context) {
        // Update the view if needed
    }
}

The UIViewRepresentable protocol requires two functions:

  • makeUIView(context:): Creates and returns an instance of your UIKit view.
  • updateUIView(_:context:): Updates the state of the UIKit view. It is called when the SwiftUI state changes.
Step 3: Use in SwiftUI

You can now use CustomUIViewRepresentable in your SwiftUI view.


import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Text("SwiftUI View")
                .font(.title)
                .padding()

            CustomUIViewRepresentable()
                .frame(width: 300, height: 200)

            Spacer()
        }
    }
}

2. Using UIViewControllerRepresentable

UIViewControllerRepresentable is used to wrap a UIViewController (a controller that manages a view hierarchy in UIKit) so it can be used in SwiftUI.

Step 1: Create a UIKit ViewController

Let’s create a simple UIViewController with a background color and a label.


import UIKit

class CustomViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        setupView()
    }

    private func setupView() {
        view.backgroundColor = .systemGreen

        let label = UILabel()
        label.text = "Custom UIKit ViewController"
        label.textColor = .white
        label.textAlignment = .center
        label.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(label)

        NSLayoutConstraint.activate([
            label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }
}
Step 2: Conform to UIViewControllerRepresentable

Create a struct that conforms to UIViewControllerRepresentable.


import SwiftUI

struct CustomUIViewControllerRepresentable: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> CustomViewController {
        return CustomViewController()
    }

    func updateUIViewController(_ uiViewController: CustomViewController, context: Context) {
        // Update the view controller if needed
    }
}

The UIViewControllerRepresentable protocol requires two functions:

  • makeUIViewController(context:): Creates and returns an instance of your UIKit view controller.
  • updateUIViewController(_:context:): Updates the state of the UIKit view controller.
Step 3: Use in SwiftUI

Now you can use CustomUIViewControllerRepresentable in your SwiftUI view.


import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Text("SwiftUI View")
                .font(.title)
                .padding()

            CustomUIViewControllerRepresentable()
                .frame(width: 300, height: 200)

            Spacer()
        }
    }
}

Passing Data Between SwiftUI and UIKit

Passing data between SwiftUI and UIKit views is a common requirement when integrating the two frameworks. Here’s how to handle data passing for both UIViewRepresentable and UIViewControllerRepresentable.

Passing Data with UIViewRepresentable

You can pass data to your UIKit view by adding properties to the UIViewRepresentable struct. The updateUIView method is then used to update the UIKit view with the new data.

Step 1: Add Properties to UIViewRepresentable

import SwiftUI

struct DataDrivenUIViewRepresentable: UIViewRepresentable {
    var text: String
    var textColor: UIColor

    func makeUIView(context: Context) -> UILabel {
        let label = UILabel()
        label.textAlignment = .center
        return label
    }

    func updateUIView(_ uiView: UILabel, context: Context) {
        uiView.text = text
        uiView.textColor = textColor
    }
}
Step 2: Use in SwiftUI with Data

import SwiftUI

struct ContentView: View {
    @State private var message: String = "Hello UIKit!"
    
    var body: some View {
        VStack {
            Text("SwiftUI View")
                .font(.title)
                .padding()

            DataDrivenUIViewRepresentable(text: message, textColor: .systemPink)
                .frame(width: 300, height: 100)

            Button("Change Text") {
                message = "Updated Text!"
            }
            .padding()

            Spacer()
        }
    }
}

Passing Data with UIViewControllerRepresentable

Similar to UIViewRepresentable, you can pass data to your UIKit view controller using properties in the UIViewControllerRepresentable struct.

Step 1: Add Properties to UIViewControllerRepresentable

import SwiftUI

struct DataDrivenUIViewControllerRepresentable: UIViewControllerRepresentable {
    var backgroundColor: UIColor

    func makeUIViewController(context: Context) -> UIViewController {
        let viewController = UIViewController()
        return viewController
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        uiViewController.view.backgroundColor = backgroundColor
    }
}
Step 2: Use in SwiftUI with Data

import SwiftUI

struct ContentView: View {
    @State private var color: Color = .blue
    
    var body: some View {
        VStack {
            Text("SwiftUI View")
                .font(.title)
                .padding()

            DataDrivenUIViewControllerRepresentable(backgroundColor: UIColor(color))
                .frame(width: 300, height: 100)

            Button("Change Color") {
                color = Color.red
            }
            .padding()

            Spacer()
        }
    }
}

Context and Coordination

Sometimes, you need more advanced interactions between SwiftUI and UIKit. This can be achieved using the Context parameter and a Coordinator class within UIViewRepresentable and UIViewControllerRepresentable.

Coordinator

The Coordinator is a helper class that facilitates communication from UIKit to SwiftUI. It is useful when you need to send data or trigger updates from the UIKit view back to the SwiftUI environment.

Step 1: Create a Coordinator

import SwiftUI

struct CoordinatorUIViewRepresentable: UIViewRepresentable {
    @Binding var text: String

    func makeUIView(context: Context) -> UITextField {
        let textField = UITextField()
        textField.delegate = context.coordinator
        return textField
    }

    func updateUIView(_ uiView: UITextField, context: Context) {
        uiView.text = text
    }

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

    class Coordinator: NSObject, UITextFieldDelegate {
        var parent: CoordinatorUIViewRepresentable

        init(_ parent: CoordinatorUIViewRepresentable) {
            self.parent = parent
        }

        func textFieldDidChangeSelection(_ textField: UITextField) {
            parent.text = textField.text ?? ""
        }
    }
}
Step 2: Use in SwiftUI

import SwiftUI

struct ContentView: View {
    @State private var text: String = ""

    var body: some View {
        VStack {
            Text("SwiftUI View")
                .font(.title)
                .padding()

            CoordinatorUIViewRepresentable(text: $text)
                .frame(width: 300, height: 50)
                .border(Color.gray)

            Text("Text: \(text)")
                .padding()

            Spacer()
        }
    }
}

Conclusion

Integrating UIKit with SwiftUI allows developers to combine the strengths of both frameworks. By using UIViewRepresentable and UIViewControllerRepresentable, you can seamlessly embed UIKit components into your SwiftUI views, pass data between them, and create advanced interactions using Coordinators. This approach is particularly useful for migrating existing UIKit projects to SwiftUI and leveraging UIKit features not yet available in SwiftUI.