0

I'm using a UITableView in SwiftUI. I get data from WebSocket and I want to update my tableview live. I successfully get the data from the network and update it in viewModel and pass it to QuotesTableView.

TableView does not update my data correctly, i.e tableViewData's first value is Symbol1 but it shows Symbol1 data in first IndexPath. I debug it in cellForRowAt function and see I get correct data, but does not show the correct item in cell. It randomly changes the order of the data in my tableview but tableviewData's order does not change.

   @StateObject private var viewModel = QuotesViewModel()

   var body: some View {
      ZStack {
         Color.init(hex: "#293c54").edgesIgnoringSafeArea(.all)

         QuotesTableView(tableViewData: $viewModel.list)
            .background(Color.clear)
      }
   }
import Combine

class QuotesViewModel: ObservableObject {
   var cancellables = Set<AnyCancellable>()
   
   // MARK: - Input
   var selectedSymbols: String
   
   /// Symbols list
   @Published var list: [SymbolsInDataModel] = .init()
   
   // MARK: - Output
   
   // MARK: - Init
   init() {
      
      socket = SocketManager.shared
      
      observeSocketValues()
      bindView()
   }
   
   // MARK: - Business Logic
   let socket: SocketManager
   
   // MARK: - Config
}

// MARK: - Bind View
extension QuotesViewModel {
   /// observe view actions in here...
   func bindView() {
      
   }
}


// MARK: - Observation Socket Data
extension QuotesViewModel {
   func observeSocketValues() {
      socket.$symbolsList.sink(receiveValue: { newSymbols in
         self.list = newSymbols
      })
      .store(in: &cancellables)

      
      socket.$symbolsList
         .filter { !$0.isEmpty }
         .first { _ in
            self.list = self.socket.symbolsList
            return true
         }
         .sink(receiveValue: {_ in})
         .store(in: &cancellables)
   }
}

struct QuotesTableView: UIViewRepresentable {   
   // The data source for the table view
   @Binding var tableViewData: [SymbolsInDataModel]
   var selectClicked: ((_ item: SymbolsInDataModel) -> Void)?
   
   func makeUIView(context: Context) -> UITableView {
      let tableView = UITableView()
      tableView.translatesAutoresizingMaskIntoConstraints = false
      tableView.backgroundColor = .clear
      tableView.dataSource = context.coordinator
      tableView.showsVerticalScrollIndicator = false
      tableView.delegate = context.coordinator
      tableView.register(HostingCell.self, forCellReuseIdentifier: "Cell")
      
      return tableView
   }
   
   func updateUIView(_ uiView: UITableView, context: Context) {
      // Reload the table view data whenever the data changes
      
      uiView.reloadData()

   }
   
   func makeCoordinator() -> Coordinator {
      Coordinator(self)
   }
 
}



// MARK: - CompetitionsTableView -> Coordinator
extension QuotesTableView {
   class Coordinator: NSObject, UITableViewDataSource, UITableViewDelegate {
      
      var parent: QuotesTableView
      
      init(_ tableView: QuotesTableView) {
         parent = tableView
      }
      
      func numberOfSections(in tableView: UITableView) -> Int {
         return 1
      }
      
      func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
         parent.tableViewData.count
      }
      
      func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
         tableView.deselectRow(at: indexPath, animated: false)
      }
      
      func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
         let tableViewCell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! HostingCell
         tableViewCell.backgroundColor = .clear
         
         print("indexpath: \(indexPath.row) item: \(parent.tableViewData[indexPath.row])")
         
         // Set the root view of the hosting controller to the view for this cell
         let row = parent.tableViewData[indexPath.row]
         let hostingController = UIHostingController(
            rootView: AnyView(
               QuotesCellView(item: row, selectAction: self.parent.selectClicked)
            )
         )
         
         hostingController.view.backgroundColor = .clear
         
         // create & setup hosting controller only once
         if tableViewCell.host == nil {
            tableViewCell.host = hostingController
            
            let tableCellViewContent = hostingController.view!
            tableCellViewContent.translatesAutoresizingMaskIntoConstraints = false
            tableViewCell.contentView.addSubview(tableCellViewContent)
            tableCellViewContent.topAnchor.constraint(equalTo: tableViewCell.contentView.topAnchor).isActive = true
            tableCellViewContent.leftAnchor.constraint(equalTo: tableViewCell.contentView.leftAnchor).isActive = true
            tableCellViewContent.bottomAnchor.constraint(equalTo: tableViewCell.contentView.bottomAnchor).isActive = true
            tableCellViewContent.rightAnchor.constraint(equalTo: tableViewCell.contentView.rightAnchor).isActive = true
         } else {
            // reused cell, so just set other SwiftUI root view
            tableViewCell.host = hostingController
         }
         tableViewCell.setNeedsLayout()
         return tableViewCell
         
      }
   }
}

Timmy
  • 4,098
  • 2
  • 14
  • 34
Kenan
  • 3
  • 2
  • When a table view cell is reused you are assigning a new hostingController but the view is not changed. So it will show the data from the last time this cell was used for a different row. – Geoff Hackworth Apr 04 '23 at 07:36
  • how can I fix it? can you show me a code example, please? – Kenan Apr 04 '23 at 08:47
  • `Coordinator(self)` is a bug, if `QuotesTableView` is re-init then `self` is an old value, `makeCoordinator` is only called once. – malhal Apr 04 '23 at 14:02

1 Answers1

0

To fix the problem with recycling cells you either need to change the data in the hosting view or create a new one. Your old code seems to want to make a new one (which is less efficient) so a small change to remove the old hosting view should fix that issue.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
         let tableViewCell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! HostingCell
         tableViewCell.backgroundColor = .clear
         
         print("indexpath: \(indexPath.row) item: \(parent.tableViewData[indexPath.row])")
         
         // Set the root view of the hosting controller to the view for this cell
         let row = parent.tableViewData[indexPath.row]
         let hostingController = UIHostingController(
            rootView: AnyView(
               QuotesCellView(item: row, selectAction: self.parent.selectClicked)
            )
         )
         
         hostingController.view.backgroundColor = .clear

         // remove any previous view if the cell is being reused
         tableViewCell.host?.view.removeFromSuperView()
         
         // create & setup hosting controller
         tableViewCell.host = hostingController
            
         let tableCellViewContent = hostingController.view!
         tableCellViewContent.translatesAutoresizingMaskIntoConstraints = false
         tableViewCell.contentView.addSubview(tableCellViewContent)
         tableCellViewContent.topAnchor.constraint(equalTo: tableViewCell.contentView.topAnchor).isActive = true
         tableCellViewContent.leftAnchor.constraint(equalTo: tableViewCell.contentView.leftAnchor).isActive = true
         tableCellViewContent.bottomAnchor.constraint(equalTo: tableViewCell.contentView.bottomAnchor).isActive = true
         tableCellViewContent.rightAnchor.constraint(equalTo: tableViewCell.contentView.rightAnchor).isActive = true
       
         tableViewCell.setNeedsLayout()
         return tableViewCell
         
      }
Geoff Hackworth
  • 2,673
  • 1
  • 16
  • 16