Designing a better UIViewControllerRepresentable [1] — Basic

enebin
5 min readApr 30, 2024

Photo by Adrien CÉSARD on Unsplash

In order to convert UIKit components into SwiftUI, you must use a specific API, UIView(Controller)Representable. As many who have used this one know, this thing is quite tricky to handle.

In this series, we will briefly examine updates regarding UIView(Controller)Representable, discuss the widely known update method using @Binding, and then introduce a way to improve the overly lengthy init that is a problem with the @Binding approach.

Set up basic components

Declare CustomViewController

First, let’s create a CustomViewController, which will serve as the basis for all subsequent examples. This component will help illustrate how to integrate and manage UIKit components within SwiftUI using UIViewControllerRepresentable.

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)")
}
}
  • The CustomViewController is a simple view controller that contains a single label and a method to update this label.

Set up PlaygroundView

Once you have set up the view as shown in the example, you will see a view like the one described below.

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))
}
}
}

When you press the button, the @State variable text changes, triggering the SwiftUI view update cycle, and the view’s body is redrawn according to the changed state. However, as you can see, the result is slightly different than expected. While the view’s text changes, the label in the ViewController does not.

Error debugging

Could this be due to the method not being called properly? To check this, let’s examine the logs printed by the debug statements that were previously embedded in the code.

Check method call

The methods seems called properly, but we encounter a different problem here. The updateUIViewController method, which is triggered every time the @State changes, is using the initial string instead of the updated one.

If we add a monitoring logic to the text variable, you can observe some quite odd behaviors, similar to what you might see in a screenshot:

@State var text: String = "" {
willSet {
print("text willSet: ", newValue)
}

didSet {
print("text didSet: ", text)
}
}

What could be the reason for this then?

Understand the ‘Source of Truth’

First, I must confess that I was mistakenly thinking the UIViewControllerRepresentable would operate in a UIKit-like manner, misled by names like makeUIViewControllerand updateUIViewController.
You know, like when you change elements in a ViewController and the screen automatically updates itself and stuff like that.

Make a long story short, it operates independently from UIKit in many respects. Also, it’s important to understand that UIViewControllerRepresentable should be seen as entirely in line with SwiftUI’s views. I couldn’t realize this sooner, as it led to a lot of wasted time.

Keep this in mind, we need to understand the concept of Source of Truth in SwiftUI’s View. Simply put, Source of Truth is a principle that dictates a view’s state should be managed internally within that view itself.

Therefore, the @State used within a subview is also intended to manage the internal state of that subview only. It should be altered by the UI elements or logic within the subview itself, and it is not possible for a parent view to directly change a subview’s @State.

In our previous code, we attempted to change the state inside the component from outside the UIViewControllerRepresentable (through the Button’s action). However, methods like makeUIViewController or updateUIViewController also operate within this logic, which explains why our code did not work as expected.

For more detailed information on this topic, check out Data Essentials in SwiftUI from WWDC20.

Solution

The simplest and most common way to address this in SwiftUI is by using @Binding. @Binding is designed for receiving state from the parent view and is linked with @State to synchronize the state between the parent and child views.

Now, let’s change the code like this:

struct CustomViewControllerRepresentable: UIViewControllerRepresentable {
let rootView = CustomViewController()
@Binding var text: String // Added

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

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

func updateText(_ text: String) -> Self {
print("updateText:", text)
self.text = text
return self
}
}

struct PlaygroundView: View {
@State var text = "Initial state"

var body: some View {
VStack {
Text("View: \(text)")

// Changed
CustomViewControllerRepresentable(text: $text)
.updateText(text)
.frame(height: 300)

Button("Update Text") {
text = "\(Date())"
}
.padding()
.foregroundColor(.white)
.background(Capsule().fill(.blue))
}
}
}

Now you can seet the codes work as expected!

Now, there are no more functional issues with the code. Those who are satisfied can wrap their project up here. However, using @Binding has a downside when you have many variables to manage internally — it forces you to write a lengthy amount of binding code.

While this might be fine for a simple view, in a view like a profile screen, where dozens of variables need to be managed, the number of bindings could increase significantly. No one wants to create a view with a long list of parameters in the init.

So, in the next post, we’ll explore how to improve this using chaining methods.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

enebin
enebin

Written by enebin

Making your dreams a reality |  Mainly Interested in iOS | https://github.com/enebin

Responses (1)

Write a response

hi Enebin, any change to share working source code as an Xcode project?