Search code examples
iosswiftuitableviewcore-datareloaddata

UITableView: reloadRows(at:) takes two hits for table to be updated and scroll every time


I have a table view (controller: MetricsViewController) which gets updated from a CoreData database. I have used prototype cells (MetricsViewCell) which I have customized for my needs. It contains a segmented control, a UIView (metricsChart, which is used to display a chart - animatedCircle), and some UILabels.

MetricsViewCell:

class MetricsViewCell: UITableViewCell {
    
    var delegate: SelectSegmentedControl?
    var animatedCircle: AnimatedCircle?
    
    @IBOutlet weak var percentageCorrect: UILabel!
    @IBOutlet weak var totalPlay: UILabel!
    
    @IBOutlet weak var metricsChart: UIView! {
        didSet {
            animatedCircle = AnimatedCircle(frame: metricsChart.bounds)
        }
    }
    @IBOutlet weak var recommendationLabel: UILabel!
    
    @IBOutlet weak var objectType: UISegmentedControl!
    
    @IBAction func displayObjectType(_ sender: UISegmentedControl) {
        delegate?.tapped(cell: self)
        
    }
}

protocol SelectSegmentedControl {
    func tapped(cell: MetricsViewCell)
}

MetricsViewController:

class MetricsViewController: FetchedResultsTableViewController, SelectSegmentedControl {

func tapped(cell: MetricsViewCell) {
    if let indexPath = tableView.indexPath(for: cell) {
        tableView.reloadRows(at: [indexPath], with: .none)
    }
}

var container: NSPersistentContainer? = (UIApplication.shared.delegate as? AppDelegate)?.persistentContainer { didSet { updateUI() } }

private var fetchedResultsController: NSFetchedResultsController<Object>?

private func updateUI() {
    if let context = container?.viewContext {
        let request: NSFetchRequest<Object> = Object.fetchRequest()
        request.sortDescriptors = []
        fetchedResultsController = NSFetchedResultsController<Object>(
            fetchRequest: request,
            managedObjectContext: context,
            sectionNameKeyPath: "game.gameIndex",
            cacheName: nil)
        try? fetchedResultsController?.performFetch()
        tableView.reloadData()
    }
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "Object Cell", for: indexPath)
    if let object = fetchedResultsController?.object(at: indexPath) {
        
        if let objectCell = cell as? MetricsViewCell {
            objectCell.delegate = self
            
            let request: NSFetchRequest<Object> = Object.fetchRequest()
            ...
            ...

            }
        }
    }
    return cell
}

When a user selects one of the segments in a certain section's segmented control, MetricsViewController should reload the data in that particular row. (There are two sections with one row each). Hence, I've defined a protocol in MetricsViewCell to inform inform my controller on user action.

Data is being updated using FetchedResultsTableViewController - which basically acts as a delegate between CoreData and TableView. Everything is fine with that, meaning I am getting the correct data into my TableView.

There are two issues:

  1. I have to tap segmented control's segment twice to reload the data in the row where segmented control was tapped.

  2. The table scrolls back up and then down every time a segment from segmented control is selected.

Help would be very much appreciated. I've depended on this community for a lot of issues I've faced during the development and am thankful already :)

For example, in Animal Recognition section, I have to hit "Intermediate" two times for its row to be reloaded (If you look closely, the first time I hit Intermediate, it gets selected for a fraction of second, then it goes back to "Basic" or whatever segment was selected first. Second time when I hit intermediate, it goes to Intermediate). Plus, the table scroll up and down, which I don't want.

enter image description here

Edit: Added more context around my usage of CoreData and persistent container.


Solution

  • Your UISegmentedControl are reusing [Default behaviour of UITableView].

    To avoid that, keep dictionary for getting and storing values.

    Another thing, try outlet connection as Action for UISegmentedControl in UIViewController itself, instead of your UITableViewCell

    The below code will not reload your tableview when you tap UISegmentedControl . You can avoid, delegates call too.

    Below codes are basic demo for UISegmentedControl. Do customise as per your need.

    var segmentDict = [Int : Int]()
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
    
        for i in 0...29 // number of rows count
        {
            segmentDict[i] = 0 //DEFAULT SELECTED SEGMENTS
        }
    
    }
    
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! SOTableViewCell
    
        cell.mySegment.selectedSegmentIndex = segmentDict[indexPath.row]!
    
        cell.selectionStyle = .none
        return cell
    }
    
    @IBAction func mySegmentAcn(_ sender: UISegmentedControl) {
    
        let cellPosition = sender.convert(CGPoint.zero, to: tblVw)
        let indPath = tblVw.indexPathForRow(at: cellPosition)
    
        segmentDict[(indPath?.row)!] = sender.selectedSegmentIndex
    
    
        print("Sender.tag     ", indPath)
    
    }