简体   繁体   中英

iOS TableView ScrollToRow Empty Screen Problem

i am trying to use scrollToRow but when call this method tableview does not show any data sometimes. When i check UI inspector i can see table cells but does not seen on screen.

I tried to DispactQueue didn't solve this problem

var forIndex = 0
    for item in filteredData {
        if let firstCharacter = item.Name?.prefix(1), firstCharacter == char {
           let indexpath = IndexPath(row: forIndex, section: 0)
           self.tableView.reloadRows(at: [indexpath], with: UITableView.RowAnimation.top)
           DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500), execute: { [weak self] in
           self?.tableView.scrollToRow(at: indexpath, at: UITableView.ScrollPosition.top, animated: true)
                    })
                    break
                }
                forIndex += 1
                }

char is element of list like a,b,c,d... FilteredData is list of tableview elements

When debugging if scrollToRow method marked breakpoint it's working

Probably the reloadRows (at:) animation is still in progress when you kick of the next causing this strange behavior.

With the breakpoint, the reloadRows (at:) has a chance to finish and on continuing the scrollToRow kicks off after.

Try one of the following if this helps your situation assuming there is no issue with your data source:

1. Change

tableView.reloadRows(at: [indexpath], with: .top)

to

tableView.reloadRows(at: [indexpath], with: .none) so that the UITableView applies a default animation

OR

2. Increase your delay before kicking off the next animation

OR

3. Perform the reload without animation

UIView.performWithoutAnimation { [weak self] in
    self?.tableView.reloadRows(at: [indexpath], with: .none)
}


tableView.scrollToRow(at: indexpath,
                            at: UITableView.ScrollPosition.top,
                            animated: true)

OR

4. Use beginUpdates and endUpdates which can be useful when performing batch operations on table views

tableView.beginUpdates()

tableView.reloadRows(at: [indexpath], with: .top)

tableView.scrollToRow(at: indexpath,
                            at: UITableView.ScrollPosition.top,
                            animated: true)

tableView.endUpdates()

Do any of these give you better results?

Update

Looking more at the docs for reloadRows(at indexPaths: , here are some lines that stood out for me

Call this method if you want to alert the user that the value of a cell is changing. If, however, notifying the user is not important—that is, you just want to change the value that a cell is displaying—you can get the cell for a particular row and set its new value.

So it seems that in some situations animation might not be needed as the cell could be off screen or not needed, so the simplest way to change data is get the cell at the index path and change the data without this function.

Since you are running this in a loop, you most likely start the next indexPath's reloadRows before the previous indexPath's scrollToRow is complete and this can cause some unusual behavior.

Since UITableView does not have it's own completion handler, we can try using CoreAnimation with recursion which can be option 5.

Here is small example I prepared. My sample is like this:

  1. I have 15 rows that are grey in color
  2. I decide that I want to change the color of 4 rows
  3. I store the index of 4 rows in a queue
  4. I will change the color using tableView.reloadRows(at:
  5. After changing the color, I want to scroll to that row using tableView.scrollToRow(at:

Here is how I accomplish that

First I extend the UITableView to use CoreAnimation block to do reloadRows and scrollToRow

extension UITableView
{
    func reloadRows(at indexPaths: [IndexPath],
                    with animation: UITableView.RowAnimation,
                    completion: (() -> Void)?)
    {
        CATransaction.begin()
        CATransaction.setCompletionBlock(completion)
        reloadRows(at: indexPaths, with: animation)
        CATransaction.commit()
    }
    
    func scrollToRow(at indexPath: IndexPath,
                     at scrollPosition: UITableView.ScrollPosition,
                     animated: Bool,
                     completion: (() -> Void)?)
    {
        CATransaction.begin()
        CATransaction.setCompletionBlock(completion)
        CATransaction.setAnimationDuration(2) // set what you want
        scrollToRow(at: indexPath, at: scrollPosition, animated: animated)
        CATransaction.commit()
    }
}

Here is how I use this extension with my view controller set up

class TableViewAnimation: UITableViewController
{
    let numberOfRows = 15
    
    // Change the color of these rows in tableview
    var colorChangeArray: [Int] = []
    
    // Copy of colorChangeArray used in recursion
    var colorChangeQueue: [Int] = []
    
    // Change the color of row to this
    private let colorToChange = UIColor.systemBlue
    
    // Normal cell color
    private let colorNormal = UIColor.gray
    
    override func viewDidLoad()
    {
        super.viewDidLoad()
        view.backgroundColor = .white
        
        setUpNavigationBar()
        setUpTableView()
    }
    
    private func setUpNavigationBar()
    {
        title = "Table View Animate"
        
        let barButton = UIBarButtonItem(title: "Animate",
                                        style: .plain,
                                        target: self,
                                        action: #selector(didTapAnimateButton))
        
        navigationItem.rightBarButtonItem = barButton
    }
    
    private func setUpTableView()
    {
        tableView.register(CustomCell.self,
                           forCellReuseIdentifier: CustomCell.identifier)
    }
    
    @objc
    func didTapAnimateButton()
    {
        // Queue all the rows that should change
        // We will dequeue these in the animation function
        // and the recursive function stops when the queue
        // is empty
        colorChangeQueue = [1, 3, 6, 12]
        
        resumeAnimation()
    }
    
    // Recursion function rather than loops using our
    // custom functions from extension
    private func resumeAnimation()
    {
        if !colorChangeQueue.isEmpty
        {
            let rowToChange = colorChangeQueue.removeFirst()
            
            print("starting \(rowToChange) animation")
            
            let indexPath = IndexPath(row: rowToChange,
                                      section: 0)
            
            colorChangeArray.append(rowToChange)
            
            tableView.reloadRows(at: [indexPath],
                                 with: .top) { [weak self] in
                
                self?.tableView.scrollToRow(at: indexPath,
                                            at: .top,
                                            animated: true,
                                            completion: {
                    
                    // recursively call the function again with a small delay
                    DispatchQueue.main.asyncAfter(deadline: .now() + 1)
                    {
                        print("complete \(rowToChange) animation")
                        self?.resumeAnimation()
                    }
                })
            }
        }
    }
}

Finally, here is the data source and delegate but nothing unique is happening here, just adding it for completeness

extension TableViewAnimation
{
    override func tableView(_ tableView: UITableView,
                            numberOfRowsInSection section: Int) -> Int
    {
        return numberOfRows
    }
    
    override func tableView(_ tableView: UITableView,
                   cellForRowAt indexPath: IndexPath) -> UITableViewCell
    {
        let cell
            = tableView.dequeueReusableCell(withIdentifier: CustomCell.identifier) as! CustomCell
        
        if colorChangeArray.contains(indexPath.row)
        {
            cell.mainView.backgroundColor = colorToChange
        }
        else
        {
            cell.mainView.backgroundColor = colorNormal
        }
        
        cell.textLabel?.textAlignment = .center
        cell.textLabel?.text = "Row \(indexPath.row)"
        
        return cell
    }
    
    override func tableView(_ tableView: UITableView,
                   heightForRowAt indexPath: IndexPath) -> CGFloat
    {
        return 150
    }
}

Now it seems like the animations of each of the row is sequenced properly:

UITableView 动画完成滚动以重新加载行 iOS Swift

And also if you see the print in the console, it is sequenced which did not happen with the loop method:

starting 1 animation
complete 1 animation
starting 3 animation
complete 3 animation
starting 6 animation
complete 6 animation
starting 12 animation
complete 12 animation

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM