iosswiftuitableviewmvvmuitableviewrowaction

Prevent tableview from being reused (MVVM )


I know how to preserve the action we have done on UITableView, after scrolling back and forth.

Now Iam doing a simple UITableView on MVVM which has a Follow button .follow button like this. Follow button changes to Unfollow after click and resets after scrolling. Where and How to add the code to prevent this?

Here is the tableview Code

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return Vm.personFollowingTableViewViewModel.count
    
}
var selectedIndexArray:[Int] = []
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    
    guard let cell = tableView.dequeueReusableCell(withIdentifier: FollowList_MVVM.PersonFollowingTableViewCell.identifier , for: indexPath) as? PersonFollowingTableViewCell else{
        return UITableViewCell()
    }


    cell.configure(with: Vm.personFollowingTableViewViewModel[indexPath.row])
    cell.delegate = self
    return cell
    
    
}

and configure(with: ) function

@objc public func didTapButton(){
    let defaultPerson = Person(name: "default", username: "default", currentFollowing: true, image: nil)
    let currentFollowing = !(person?.currentFollowing ?? false)
    person?.currentFollowing = currentFollowing
    delegate?.PersonFollowingTableViewCell(self, didTapWith: person ?? defaultPerson )    
    configure(with: person ?? defaultPerson)
}


func configure(with person1 : Person){
    
    
    self.person = person1
    nameLabel.text = person1.name
    usernameLabel.text = person1.username
    userImageview.image = person1.image
    
    
    
    if person1.currentFollowing{
        //Code to change button UI
        
    }

custom delegate of type Person is used


Solution

  • I guess your main issue is with Button title getting changed on scroll, so i am posting a solution for that.

    Note-: Below code doesn’t follow MVVM.

    Controller-:

    import UIKit
    
    class TestController: UIViewController {
        
        @IBOutlet weak var testTableView: UITableView!
        var model:[Model] = []
        
        override func viewDidLoad() {
            for i in 0..<70{
                let modelObject = Model(name: "A\(i)", "Follow")
                model.append(modelObject)
            }
        }
    }
    
    extension TestController:UITableViewDelegate,UITableViewDataSource{
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            
            return model.count
        }
        
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! TestTableCell
            cell.dataModel = model[indexPath.row]
            cell.delegate = self
            return cell
        }
        
        func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
            return 100
        }
        
    }
    
    
    extension TestController:Actions{
        func followButton(cell: UITableViewCell) {
            let indexPath = testTableView.indexPath(for: cell)
            model[indexPath!.row].buttonTitle = "Unfollow"
            testTableView.reloadRows(at: [indexPath!], with: .automatic)
        }
    }
    
    class Model{
        var name: String?
        var buttonTitle: String
        
        init(name: String?,_ buttonTitle:String) {
            self.name = name
            self.buttonTitle = buttonTitle
        }
    }
    

    Cell-:

    import UIKit
    
    protocol Actions:AnyObject{
        func followButton(cell:UITableViewCell)
    }
    
    class TestTableCell: UITableViewCell {
        
        @IBOutlet weak var followButtonLabel: UIButton!
        @IBOutlet weak var eventLabel: UILabel!
        
        var dataModel:Model?{
            didSet{
                guard let model = dataModel else{
                    return
                }
                
                followButtonLabel.setTitle(model.buttonTitle, for: .normal)
                eventLabel.text = model.name
            }
        }
        
        weak var delegate:Actions?
        
        override func awakeFromNib() {
            super.awakeFromNib()
            // Initialization code
        }
        
        override func setSelected(_ selected: Bool, animated: Bool) {
            super.setSelected(selected, animated: animated)
            
            // Configure the view for the selected state
        }
        
        @IBAction func followAction(_ sender: Any) {
            delegate?.followButton(cell:self)
        }
    }
    

    To convert this into MVVM approach, there are few things you need to change and move out.

    1. The loop I have in viewDidLoad shouldn’t be there. That will be some API call, and should be handled by viewModel, and viewModel can delegate that to other repository to handle or handle itself. Upon receiving response viewModel update its state and communicate with View (in our case tableView) to re-render itself.

    2. Code in extension where I am updating model object shouldn’t be in controller (model[indexPath!.row].buttonTitle = "Unfollow"), that has to be done by viewModel, and once the viewModel state changes it should communicate with view to re-render.

    3. The interaction responder (Button action) in Cell class, should delegate action to viewModel and not controller.

    4. Model class should be in its own separate file.

    In short viewModel handles the State of your View and it should be the one watching your model for updates, and upon change it should ask View to re-render.

    There are more things you could do to follow strict MVVM approach and make your code more loosely coupled and testable. Above points might not be 100% correct I have just shared some basic ideas i have. You can check article online for further follow up.