UIView (UIButton) Won't re-render after label updates

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()
    }
}
Answered by MobileTen in 775463022

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 in UIKit, this won't work. Because the View Controller is being updated by the feedback from updateUIViewController 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 the updateUIViewController life cycle updates.
Accepted Answer

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 in UIKit, this won't work. Because the View Controller is being updated by the feedback from updateUIViewController 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 the updateUIViewController life cycle updates.
UIView (UIButton) Won't re-render after label updates
 
 
Q