swiftxcodeuitableviewxcode14

How to listen for data change with @Published variable then reload tableView


The most difficult task I face is to know the correct terminology to search for. I'm used to SwiftUI for an easy way to build an app in the fastest time possible. With this project I have to use UIKit and for this specific task.

Inside a view controller I created a tableView:

private let tableView: UITableView = {
    let table = UITableView()
    table.register(ProfileCell.self, forCellReuseIdentifier: ProfileCell.identifier)
    return table
}()

Later I reload the data inside viewDidLoad


override func viewDidLoad() {
    super.viewDidLoad()
    
    Task {
        do {
            try await viewModel.getProfiles()

            // Here I reload the table when data comes in
            self.tableView.reloadData()
        } catch {
            print(error)
        }
    }
    
    view.addSubview(tableView)
    tableView.delegate = self
    tableView.dataSource = self
}

So what is viewModel? In SwiftUI I'm used to having this inside a view struct:


@ObservedObject var viewModel = ProfilesViewModel()

..and that's what I have inside my view controller. I've searched for:

..and more but noting useful for me to "pick up the pieces" with.

In same controller, I'm showMyViewControllerInACustomizedSheet which now uses UIHostingController:


private func showMyViewControllerInACustomizedSheet() {
    // A SwiftUI view along with viewModel being passed in
    let view = ProfilesMenu(viewModel: viewModel)
    let viewControllerToPresent = UIHostingController(rootView: view)
    if let sheet = viewControllerToPresent.sheetPresentationController {
        sheet.detents = [.medium(), .large()]
        sheet.largestUndimmedDetentIdentifier = .medium
        sheet.prefersScrollingExpandsWhenScrolledToEdge = false
        sheet.prefersEdgeAttachedInCompactHeight = true
        sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = true
    }
    present(viewControllerToPresent, animated: true, completion: nil)
}

For the ProfilesViewModel:


class ProfilesViewModel: ObservableObject {

  // ProfilesResponse is omitted
  @Published var profiles = [ProfilesResponse]()

  public func getProfiles(endpoint: String? = nil) async throws -> Void {
    
   // After getting the data, I set the profiles variable
   self.profiles = [..]
  }
}

Whenever I call try await viewModel.getProfiles(endpoint: "..."), from ProfileMenu, I'd like to reload the tableView. What additional setup is required?


Solution

  • In the comments, Vadian mentioned "Combine" where I did a Google search and found this. What works, for a basic demonstaration:

    
    [..]
    import Combine
    
    class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
      private let viewModel = ProfilesViewModel()
      private var cancellable: AnyCancellable?
    
      override func viewDidLoad() {
          super.viewDidLoad()
          
          Task {
              do {
                  try await viewModel.getProfiles()
    
                  // Remove this
                  // self.tableView.reloadData()
              } catch {
                  print(error)
              }
    
    
          }
          
          view.addSubview(tableView)
          tableView.delegate = self
          tableView.dataSource = self
    
          // Add this
          cancellable = viewModel.objectWillChange.sink(receiveValue: { [weak self] in
            self?.render()
          })
      }
    
      // Also add this
      private func render() {
        // TODO: Implement failures...
        DispatchQueue.main.async {
            self.tableView.reloadData()
        }
      }
    
      ...
    }
    
    

    objectWillChange was the key to my problem.