Search code examples
iosuikitios-simulator

Strange bounce in UITableView running on iPhone 14 Pro but not iPhone 14


There's a strange bounce when my app is run on iPhone 14 Pro (and Pro Max) models but not any other models. How can I remove it?

The source code is here: https://github.com/ykphuah/ListBouncePro

Refer to the video below. Left is the iPhone 14 simulator, while right is the iPhone 14 pro simulator. There's a bounce on the main listing after the sheet is dismissed.

enter image description here


Solution

  • First, the reason the issue seems to be related to CoreData is because this implementation causes the table view to update while it is covered by the presented controller.

    You can easily confirm this by triggering a reload while the Edit controller is presented:

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if (segue.identifier == "edit") {
            if let indexPath = tableView.indexPathForSelectedRow {
                let controller = (segue.destination as! UINavigationController).topViewController as! EditController
                controller.isNew = false
                controller.shopping = frc?.object(at: indexPath)
                tableView.deselectRow(at: indexPath, animated: false)
                
                // add this to trigger a reload after 1 second (while the presented controller is still showing)
                DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: {
                    self.tableView.reloadData()
                })
                
            } else {
                let controller = (segue.destination as! UINavigationController).topViewController as! EditController
                controller.isNew = true
            }
        }
    }
    

    Now, even if you only tap "Cancel" the gap will be there.

    And, you can confirm this by setting up:

    enter image description here

    With this as the entire code:

    class NoCoreDataTableViewController: UITableViewController {
        
        override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return 5
        }
        override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let c = tableView.dequeueReusableCell(withIdentifier: "defCell", for: indexPath)
            c.textLabel?.text = "\(indexPath)"
            return c
        }
        
        override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
            if let indexPath = tableView.indexPathForSelectedRow {
                tableView.deselectRow(at: indexPath, animated: false)
                DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: {
                    self.tableView.reloadData()
                })
            }
        }
    }
    

    When run, pulling down the presented controller shows the gap:

    enter image description here

    and the gap remains after pulling it down far enough to dismiss it.

    The only thing I've found that seems reliable is to implement scrollViewDidChangeAdjustedContentInset(...) and adjust the table view's .contentInset.top to "fix" the incorrect inset.

    If you add this to your ListController class:

    // class property
    var origTopInset: CGFloat = 0
    
    override func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView) {
        if origTopInset == 0 {
            origTopInset = scrollView.adjustedContentInset.top
        }
        if scrollView.adjustedContentInset.top != origTopInset {
            scrollView.contentInset.top = -abs(origTopInset - scrollView.adjustedContentInset.top)
        }
    }
    

    that should get rid of the gap / bounce. If the app is running on a different device (such as iPhone 14 (non-Pro) from your original post), the .adjustedContentInset.top won't change, and so this work-around code won't adversely affect anything.

    Note that I've only done cursory testing -- so thoroughly test this yourself.


    Edit

    The above .adjustedContentInset.top "fix" only works if the row updates are set to .none instead of .fade...

    I've re-updated my repo clone: https://github.com/DonMag/ListBouncePro with a "Closures" branch for another approach.

    I had seen cases where it still failed, but I didn't realize that Core Data calls didChange when the object changes ... such as:

    shopping?.item = textField.text
    

    I was under the impression it did not occur until:

    appDelegate.saveContext()
    

    I've re-posted my clone of your repo: https://github.com/DonMag/ListBouncePro with a "Closures" branch.

    The changes are:

    • instead of passing a reference to the Core Data object to the Edit controller, we pass
      • the item string (would probably be changed to a struct, assuming you'll have more than one piece of information)
      • the indexPath if an item was selected, or nil if "+" new
    • Two closures - one for Save and one for Delete

    All Core Data functions then take place in the List controller, which solves the "bouncing" / gap.

    Now the tableView updating takes place after the presented controller is dismissed. That might be considered a drawback ... or maybe not. Since the data is being sorted, the user may find this easier to follow when seeing the update, rather than returning to a re-ordered list.

    As a side note -- this may be more effort than it's worth, and may not be completely compatible with the rest of your data handling.

    Frankly, the "bounce" is - to me - a non-issue. And, since this is pretty clearly a bug that will hopefully be corrected in an iOS update, any work put in to work around it would eventual be unnecessary (and maybe even counter-productive).

    However, take a look at the "Closure" branch and see what you think... you might even decide that using your Edit controller only for editing and not for actual data updating is a better approach all around.