Designing a better UIViewControllerRepresentable [2] — Without @Binding

Enebin
4 min readMay 1, 2024

--

Photo by Adrien CÉSARD on Unsplash

In the previous post, we examined the simplest method using @Binding. However, as mentioned before, the main downside to this approach is that you can end up passing a huge number of @Bindings through init.

To avoid this and allow the view to adhere more closely to SwiftUI’s declarative paradigm, let’s add a little trick to improve the situation.

We will continue using the same UIViewController and View code that we used previously.

class CustomViewController: UIViewController {
var label: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
label = UILabel()
label.textAlignment = .center
view.addSubview(label)

label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}

func updateLabelText(to newText: String) {
label.text = newText
print("updateLabelText: \(newText)")
}
}

struct PlaygroundView: View {
@State var text = "Initial state"
var body: some View {
VStack {
Text("View: \(text)")
CustomViewControllerRepresentable()
.updateText(text)
.frame(height: 300)
Button("Update Text") {
text = "\(Date())"
}
.padding()
.foregroundColor(.white)
.background(Capsule().fill(.blue))
}
}
}

New method: Copy the `self`

Implementation of UIViewControllerRepresentable will be slightly different. Specifically, we’ll make some changes to the internal logic of updateText.

struct CustomViewControllerRepresentable: UIViewControllerRepresentable {
var labelText = ""

func makeUIViewController(context: Context) -> CustomViewController {
CustomViewController()
}

func updateUIViewController(_ uiViewController: CustomViewController, context: Context) {
uiViewController.updateLabelText(to: labelText)
}

// Added
func updateText(_ text: String) -> Self {
var copy = self
copy.labelText = text
return copy
}
}

Unlike the previous code that tried to modify @State or @Binding, the new code copies self, changes the properties internally, and then returns the instance.

In the old code, we used @Binding to make SwiftUI’s view lifecycle call updateUIViewController to update the view. However, this code uses a strategy where every time the parent view’s @State changes, the updateUIViewController is called to update the text.

It works as expected!

Note

  • I’ve prepared visual aids about how the views are updated to help you understand.

Note

  • It’s important to note that makeUIViewController is called only once. This is a intended behavior by SwiftUI’s policy.
  • Therefore, if you treat makeUIViewController as if it were init, it could lead to problems. For more details, check the official documentation.

Note

  • The reason for creating a copy of the instance is to avoid making the method a mutating method. If it becomes a mutating method, it cannot be used within the body of the View.
  • Using this approach not only allows the method to be used inside the View’s body, similar to other SwiftUI methods like background and overlay, but it also offers chaining, enabling the continuous calling of other methods.

Upgrade: Expose view controller using closure

In fact, the work to change the text is now complete. However, you might want more control over the CustomViewController. In that case, you can use closures to modify it like this:

struct CustomViewControllerRepresentable: UIViewControllerRepresentable {
typealias Configuration = (CustomViewController) -> Void

var configuration: Configuration?

func makeUIViewController(context: Context) -> CustomViewController {
CustomViewController()
}

func updateUIViewController(_ uiViewController: CustomViewController, context: Context) {
configuration?(uiViewController)
}

func configure(_ configuration: @escaping Configuration) -> Self {
var copy = self
copy.configuration = configuration
return copy
}
}

Now, the View can be written as:

CustomViewControllerRepresentable()
.configure { viewController in
viewController.updateLabelText(to: text)
}

Upgrade: Make it generic

What if you target a more generic view controller rather than CustomViewController? This change will provide a faster and simpler approach for all view controllers.

struct SwiftUIViewControllerRepresentable<Content: ViewControllerTypeProtocol>: UIViewControllerRepresentable {
typealias Configuration = (Content) -> Void

var makeContent: () -> Content
var configuration: Configuration?

init(makeContent: @escaping () -> Content) {
self.makeContent = makeContent
}

func makeUIViewController(context: Context) -> Content {
makeContent()
}

func updateUIViewController(_ uiViewController: Content, context: Context) {
configuration?(uiViewController)
}

func configure(_ configuration: @escaping Configuration) -> Self {
var copy = self
copy.configuration = configuration
return copy
}
}

extension ViewControllerTypeProtocol {
static func swiftUI(makeView: @escaping () -> Self) -> SwiftUIViewController<Self> {
SwiftUIViewControllerRepresentable(makeContent: makeView)
}
}
/// A protocol that all `UIView`s conform to, enabling extensions that have a `Self` reference.
protocol ViewControllerTypeProtocol: UIViewController {}
extension UIViewController: ViewControllerTypeProtocol {}

Now, the View can be written as:

CustomViewController.swiftUI {
CustomViewController()
}
.configure { viewController in
viewController.updateLabelText(to: text)
}

--

--