Designing a better UIViewControllerRepresentable [2] — Without @Binding
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 @Binding
s 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 wereinit
, 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 amutating
method, it cannot be used within thebody
of theView
. - Using this approach not only allows the method to be used inside the
View
’sbody
, similar to other SwiftUI methods likebackground
andoverlay
, 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)
}
References
Official document
Else
- I must confess that I referenced Lottie a lot for this method. Lottie has implemented some really cool SwiftUI code. It helped me a lot.