Situation
I am implementing a filter view that has a button to apply the filters selected. The button shows how many results applying the current filters would yield. Like this: The label contains the count variable, the expectation is that when the count updates the button will reflect the new number of results.
Current setup
I am passing the data (count displayed in the button) like this: ViewModel -> View (SwiftUI) -> View (SwiftUI) -> UIViewControllerRepresentable -> UIViewController. I have validated that the variable that holds the count updates whenever a filter is selected, but the button is not re-rendered to display the new value.
Here are some simplified code snippets (In the order described above):
ViewModel:
@MainActor class ViewModel: ObservableObject {
@Published var resultCount: Int = 0
@Published var status: String = ""
@Published var type: String = ""
init(){
self.addSubscribers()
})
}
func addSubscribers() {
// code here used to filter uses sink based on filters (status and type) selected, this will update resultCount
}
}
ContainerView:
struct ContainerView: View {
@EnvironmentObject private var vm: ViewModel
var body: some View {
VStack {
FilterView(status: $vm.status, type: $vm.type, resultCount: $vm.resultCount)
}
}
}
FilterView:
struct FilterView: View {
@Binding var status: String
@Binding var type: String
@Binding var resultCount: Int
var body: some View {
VStack {
FormViewControllerRepresentable(status: $status, type: $type, resultCount: $resultCount, showFilterSelectionView: $showFilterSelectionView)
}
}
}
FormViewControllerRepresentable:
struct FormViewControllerRepresentable: UIViewControllerRepresentable {
@Binding var status: String
@Binding var type: String
@Binding var resultCount: Int
func makeUIViewController(context: Context) -> FilterFormViewController {
FilterFormViewController(status: $status, type: $type, resultCount: $resultCount)
}
func updateUIViewController(_ uiViewController: FilterFormViewController, context: Context) {
uiViewController.status = status
uiViewController.type = type
uiViewController.resultCount = resultCount
}
}
UIViewController
class FilterFormViewController: UIViewController, UITextFieldDelegate {
@Binding var status: String
@Binding var type: String
@Binding var resultCount: Int
@Binding var showFilterSelectionView: Bool
init(status: Binding<String>, type: Binding<String>, resultCount: Binding<Int>, showFilterSelectionView: Binding<Bool>) {
self._status = status
self._type = type
self._resultCount = resultCount
self._showFilterSelectionView = showFilterSelectionView
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
lazy var formView = Form()
let stackView = UIStackView()
let scrollView = UIScrollView()
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
func setup() {
// set delegate for Picker (filters)
formView.statusPicker.textField.delegate = self
formView.statusPicker.delegate = self
formView.typePicker.textField.delegate = self
formView.typePicker.delegate = self
stackView.addArrangedSubview(getButtonsStackView())
scrollView.addSubviews([stackView])
view.addSubview(scrollView)
}
func getButtonsStackView() -> UIStackView {
// HERE is the button. even though resultCount updates, the button is not re-rendered
// UIButton here is actually a custom component that conforms to UIButton. In the custom component setTitle(label, for: .normal) is called
let submitButton = UIButton( label: "Show \(self.resultCount) results")
submitButton.isEnabled = true
submitButton.addTarget(self, action: #selector(hideFilters), for: .touchUpInside)
let buttonsView = UIStackView(
arrangedSubviews: [
submitButton
]
)
return buttonsView
}
func textFieldDidEndEditing(_ textField: UITextField) {
// getters here are working well and setting the value for status & type is updating the variable in the viewModel
// THE PROBLEM IS: While this updates the value for the filters and that in turns updates the result count, the button won't re-render with the new value
status = formView.getStatus()
type = formView.getType()
}
}
Change this
class FilterFormViewController: UIViewController, UITextFieldDelegate {
@Binding var status: String
@Binding var type: String
@Binding var resultCount: Int
@Binding var showFilterSelectionView: Bool
init(status: Binding<String>, type: Binding<String>, resultCount: Binding<Int>, showFilterSelectionView: Binding<Bool>) {
self._status = status
self._type = type
self._resultCount = resultCount
self._showFilterSelectionView = showFilterSelectionView
super.init(nibName: nil, bundle: nil)
}
To
class FilterFormViewController: UIViewController, UITextFieldDelegate {
var status: String {
didSet {
// assign status to UIKit control
}
}
var type: String {
didSet {
// assign type to UIKit control
}
}
var resultCount: Int {
didSet {
// assign resultCount to UIKit control
submitButton.setTitle("Show \(self.resultCount) results", state: .normal)
}
}
var showFilterSelectionView: Bool
let submitButton: UIButton
init(status: String, type: String, resultCount: Int, showFilterSelectionView: Bool) {
self.status = status
self.type = type
self.resultCount = resultCount
self.showFilterSelectionView = showFilterSelectionView
super.init(nibName: nil, bundle: nil)
submitButton = UIButton()
submitButton.isEnabled = true
submitButton.addTarget(self, action: #selector(hideFilters), for: .touchUpInside)
}
- You're trying to use
SwiftUI
patterns inUIKit
, this won't work. Because the View Controller is being updated by the feedback fromupdateUIViewController
method of the UIView Controller Representable in response to incoming changes from the view model publishers. The view model is pushing data down or over to the view controller not from view controller to view model. If you want to push data from the view controller back to the swiftui view model, you will need to delegate protocols or call back handlers so when the data changes on the UIKit side of the code the call back handlers will pass this data back to the Bindings independent of theupdateUIViewController
life cycle updates.