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 makeUIViewController
and 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.