How to access / use a @Binding var someValue
of an UIViewControllerRepresentable
within its Coordinator
?
I found several examples which simply pass the Binding
to the Coordinator
. While this seems to work, I wonder if it is correct or might break in the future. As far as I know Bindings
should only be used within Views
.
Have a look at the following demo code. Is the Binding
uses/accessed correctly? Or is there a better way?
struct PickerView: UIViewControllerRepresentable {
@Binding var selectedIndex: Int
init(selectedIndex: Binding<Int>) {
self._selectedIndex = selectedIndex
}
func makeCoordinator() -> Coordinator {
return Coordinator(selectedIndex: $selectedIndex) // <- correct?
}
func makeUIViewController(context: Context) -> UIViewController {
let pickerVC = PickerViewController()
pickerVC.delegate = context.coordinator
return pickerVC
}
func updateUIViewController(_ viewController: UIViewController, context: Context) {
// ...
}
class Coordinator: NSObject, PickerDelegate {
var selectedIndex: Binding<Int> // <- correct?
init(selectedIndex: Binding<Int>) {
self.selectedIndex = selectedIndex
}
func didSelectItem(at index: Int) {
selectedIndex.wrappedValue = index // <- correct?
}
}
}
// UIKit ViewController
protocol PickerDelegate: AnyObject {
func didSelectItem(at index: Int)
}
class PickerViewController: UIViewController {
var delegate: PickerDelegate?
func selectItem(at index: Int) {
delegate?.didSelectItem(at: index)
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
let button = UIButton(type: .system)
button.setTitle("Pick Random Index", for: .normal)
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(button)
NSLayoutConstraint.activate([
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
button.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
@objc private func buttonTapped() {
let randomIndex = Int.random(in: 0...10)
print("Button tapped → random index: \(randomIndex)")
selectItem(at: randomIndex)
}
}
// Demo View
struct PickerDemoView: View {
@State private var selectedIndex = 0
var body: some View {
VStack(spacing: 20) {
Text("Selected Index: \(selectedIndex)")
.font(.largeTitle)
Divider()
PickerView(selectedIndex: $selectedIndex)
.frame(height: 200)
.border(Color.blue)
}
.padding()
}
}
I don't think it is inherently a problem to have a Binding
inside the coordinator, but I don't like it in terms of style. The important thing you need to do is to update the binding in updateUIViewController
.
func updateUIViewController(_ viewController: UIViewController, context: Context) {
context.coordinator.selectedIndex = $selectedIndex
}
class Coordinator: NSObject, PickerDelegate {
var selectedIndex: Binding<Int>?
// I recommend just using the parameterless initialiser generated by the compiler.
// Passing the binding through the initialiser makes it easy to forget to update the binding property in updateUIViewController.
// init(selectedIndex: Binding<Int>) {
// self.selectedIndex = selectedIndex
// }
func didSelectItem(at index: Int) {
selectedIndex?.wrappedValue = index
}
}
This is necessary because the SwiftUI side might happen to be using a "different" Binding
after a view update. This keeps the UIKit side "in sync" with the SwiftUI side.
I personally don't like to see SwiftUI property wrappers in the coordinator. I would typically do:
func updateUIViewController(_ viewController: UIViewController, context: Context) {
context.coordinator.selectedIndexDidChange = { selectedIndex = $0 }
}
class Coordinator: NSObject, PickerDelegate {
var selectedIndexDidChange: ((Int) -> Void)?
func didSelectItem(at index: Int) {
selectedIndexDidChange?(index)
}
}
The line in updateUIViewController
is effectively doing the same thing as updating the Binding
property. It's just there is no Binding
property - the updated Binding
is captured in a closure.