6

I have a normal UITableView in my UIViewController, constrained to fullscreen. This tableView has a custom interactive tableHeaderView. The tableHeaderView is dynamic in size, and expands on its own. The headerView has a textField, and will change its own size depending on wether the textField has focus or not.

The problem is that the cells at the bottom of the screen aren't always animating correctly when changing the size of the tableHeaderView. I am calling tableView.beginUpdates() and tableView.endUpdates() after layoutIfNeeded() inside my animation-block. This has to be done, or the cells won't follow the dynamic size at all.

I've made this gif. Look specifically at the bottom cells during the animation. I have slowed down the animation considerably, so it's easy to see the problem.

gif of problem

Speculation: It seems to me like the tableView calls for cellForRow:indexPath: as soon as the animation starts, and somehow finds out what state the entire tableView will be in after the animation, and removing the unnecessary cells, even though the animation has not yet completed. The same way, when collapsing the header: the bottommost cells are not animated in the same way as the already loaded cells. They are animated in with a different animation..

Why is this happening? Is this preventable?

Edit: Animation code

self.navigationController?.setNavigationBarHidden(isEditing, animated: true)

var frame = tableHeaderView.frame
frame.size.height = tableHeaderView.headerHeight(forEditing: isEditing)

UIView.animate(withDuration: 0.3, animations: {

    if isEditing{
        let point = CGPoint(x: 0, y: -self.topLayoutGuide.length)
        self.tableView.setContentOffset(point, animated: false)
    }    

    tableHeaderView.frame = frame
    tableHeaderView.layoutIfNeeded()
    self.view.layoutIfNeeded()
    self.tableView.beginUpdates()
    self.tableView.endUpdates()
}) { [weak self](completed:Bool) in
    //Do stuff
}
Sti
  • 8,275
  • 9
  • 62
  • 124
  • 2
    can you show the code? – jalone Jan 30 '17 at 10:13
  • In storyboard - select scene ->show attribute inspector -> under top bars , make untick in under top bar . – Pavankumar Jan 30 '17 at 10:19
  • @Sti : Use should share some code. – Poles Jan 30 '17 at 10:30
  • @jalone not much code to show, but I have updated the question with some. – Sti Jan 30 '17 at 10:37
  • @Poles A bit of animation code added to question – Sti Jan 30 '17 at 10:37
  • Why did you `endUpdates` right after `beginUpdates`? I think you have to use `beginUpdates` before animation code and `endUpdates` inside `completed:Bool` section. – Poles Jan 30 '17 at 10:42
  • @Poles I only call `beginUpdates` and `endUpdates` to notify the tableView that something has changed. If I put `beginUpdates` before the animation code, and `endUpdates` in the completion, all the cells scroll into place *after* the animation is completed. And still with the same bug. If I put `beginUpdates` before the animation and `endUpdates` inside the animation, it also doesn't work. For one, the sectionHeader (the segmentedControl) isn't animated. To call `endUpdates` right after `beginUpdates` was something I picked up from another StackOverflow-question somewhere, and it works. – Sti Jan 30 '17 at 10:53
  • Not too much code to understand what you are doing. Do you remove cells in table? This should happen between beginUpdates/endUpdates calls. – jesse Jan 30 '17 at 11:48
  • @jesse No, I'm not removing anything. This is pretty much the only code. It is as simple as it sounds. I have a regular tableView, with a custom UIView as tableHeaderView. I listen for when the UITextField becomes firstResponder, and call the function above when it does, with the parameter `isEditing` set to true if I want to collapse the headerView.. Not much more code to show.. – Sti Jan 30 '17 at 11:53
  • Then you don't need beginUpdates/endUpdates. – jesse Jan 30 '17 at 12:06
  • @jesse If I remove beginUpdates and endUpdates, the cells won't move with the size of the headerview.. Like.. The tableHeaderView will collapse, but the cells will stay in the same place as they were. There will be a big space between the tableHeader and the cells. Calling beginUpdates and endUpdates just notifies the tableView that something should've changed. And it has. There doesn't have to be anything between the two. Only removal and adding of cells and such should be between, and I do nothing of that. – Sti Jan 30 '17 at 12:20
  • And why are you changing contentOffset of tableView? I don't see the purpose of it. – jesse Jan 30 '17 at 13:12
  • @jesse That's a few very important lines of code. It's to make sure the tableHeaderView is at the top of the screen and not scrolled away after the animation.. If I scroll a bit down before I tap the `UITextField` (and the header collapses), then the tableView will keep its contentOffset, and the header will disappear behind the navigationBar.. Remember, the tableHeaderView is also within the scrollable content. It's to make sure the textField is always at the same position after the animation. – Sti Jan 30 '17 at 13:18
  • I have just submitted a Code-Level Technical Support Incident with Apple to investigate this issue because it sounds like it’s a bug in UIKit, and it’s driving me nuts too. Once I hear back from them, I’ll be sure to share my findings. – gomollon Nov 17 '17 at 02:30

2 Answers2

1

I just heard back from the Apple Developer Technical Support team regarding this issue, and here's their response verbatim (emphasis mine):

Hello Nicolas,

Thank you for contacting Apple Developer Technical Support (DTS).

The behavior and resulting limitations you describe are by design.

If you believe an alternative approach should be considered by Apple, we encourage you to file an enhancement request with information on how this design decision impacts you, and what you’d like to see done differently.

Although there is no promise that the behavior will be changed, it is the best way to ensure your thoughts on the matter are seen by the team responsible for the decision.

Best Regards,

Apple Developer Technical Support

It seems we're just stuck with having to use a simple call to either tableView.setContentOffset(_:animated:) or tableView.scrollToRow(at:at:animated:).

gomollon
  • 503
  • 3
  • 14
  • How would setContentOffset or scrollToRow fix this? – Sti Dec 06 '17 at 08:59
  • It doesn't—we're just stuck with using those standard method calls without the ability to sync it up with a custom animation—which is why the team suggests we file an enhancement request for it. In other words, you can "fix" it by moving your call to either of those methods outside of the animation block. – gomollon Dec 09 '17 at 01:20
0

Ok, I'll try to propose my version, though I can't check it, since I don't see your project. I didn't try to compile it, so it may have mistakes.

var navFrame = self.navigationController?.navigationBar.frame
navFrame.origin.y = isEditing ? -navFrame.size.height : 0

var frame = tableHeaderView.frame
frame.size.height = tableHeaderView.headerHeight(forEditing: isEditing)

UIView.animate(withDuration: 0.3, animations: {
    if isEditing{
        let point = CGPoint(x: 0, y: -self.topLayoutGuide.length)
        self.tableView.contentOffset = point
    }

    self.navigationController?.navigationBar.frame = navFrame

    tableHeaderView.frame = frame

    self.view.layoutIfNeeded()
}) { [weak self](completed:Bool) in
    //Do stuff
}
jesse
  • 650
  • 5
  • 19
  • Thanks, I'll try this when I get the time to try. One question though, why manually hide the navigationBar instead of using `self.navigationController?.setNavigationBarHidden(isEditing, animated: true)` like I already had? You think that can be related to the problem? – Sti Jan 30 '17 at 14:13
  • @Sti Yeah, probably animations conflict. – jesse Jan 30 '17 at 14:24