16

I have a problem that stems from the fact that UITableViewController refreshControl is glitchy when the frame of the UITableViewController is below a certain height.

As it stands, I have a UIViewController, and in it I have a ContainerView that embeds a UITableViewController. I want the height to be 50% of the screen.

When I use the refreshControl, I get this kind of behavior: The tableView jumps down at the very end when scrolling down. You'll notice it towards the end of this video when I decide to scroll down slowly.

This problem does not occur when the ContainerView frame is above a certain value. So, when the height is 75% of the screen, everything works perfectly and the refreshControl is smooth. When it is 50%, then that bug happens.

Two different things I have tried:

  1. self.tableView.frame = CGRectMake(0, numOfPixelsToDropTableBy, self.tableView.frame.size.width, self.tableView.frame.size.height) is one thing that I tried. The problem with this is if you want to give the tableView rounded corners via the ContainerView and the fact that your ContainerView still takes up more space and this makes constraints for other elements awkward.

  2. I went to the Storyboard and I basically had the top of the ContainerView where I wanted. Then, I had the bottom extend beyond the bottom of the screen to give the ContainerView a large enough height... but the user would never know. Except, they would know because now the tableView extends beyond the screen and I can't see the last few rows of my tableView.

Ultimately... I don't want to use a 3rd-party library, but I want a perfectly functioning refreshControl. How can I fix this?

David
  • 7,028
  • 10
  • 48
  • 95
  • Can you check the behavior with different cell sizes? Example, make the cells 4x height and see behavior, then make them 1/4th height and see behavior. I am wondering if that somehow effects it as well. – Kris Gellci Sep 15 '15 at 23:04
  • @KrisGellci Yep, doesn't matter what the cell size is. – David Sep 16 '15 at 00:59
  • @David Have you tried my solution? – Alexey Bondarchuk Sep 27 '15 at 07:15
  • Having a play, the jump seems to happen at the point at which the value changed event is sent which is triggered by how far you have pulled down. The jump down happens after the event fires it seems. I think this is because you have continued to scroll and it is catching up with the positions it lost while in the change value callback. So scroll, pause for event then catchup with scrolling. It is not really intended to be a slow motion refresh feature: More a quick pull and release gesture. – Rory McKinnel Sep 28 '15 at 14:33
  • Think I found that the issue relates to margin based constraints. See answer. It is very reproducible. – Rory McKinnel Sep 28 '15 at 16:36
  • @AlexeyBondarchuk Sorry, I wasn't given time, but looking at it, I knew the issue would still come up. As Rory said below, it is due to margin constraints. – David Sep 28 '15 at 16:36
  • @David Found a kind of workaround by applying a scaling transform to the embedded navigation controller. See updated answer. – Rory McKinnel Sep 30 '15 at 22:27
  • Was this problem solved? – Caleb Kleveter Sep 30 '15 at 22:50
  • @CalebKleveter Not unless David has fixed it some other way. There seems to be no fix and workarounds are thin on the ground as far as I can work out. – Rory McKinnel Sep 30 '15 at 22:53
  • I feel your pain. Unfortunately, using the standard refreshControl outside of a UITableVIewController, or in non-standard circumstances will make your life miserable. I tried many things, and even adjusting the tintColor of the refreshControl is glitchy: sometimes it works, sometimes it doesn't. Sometimes it needs to be changed inside an animation block, sometimes not. Eventually I spend a weekend implementing a generic solution for this: have an object keep track of the contentOffset, and have a subclassable refreshView. If you are interested, I can put this on github. – Joride Oct 01 '15 at 18:03
  • @Joride I'd love to take a look – David Oct 01 '15 at 18:10

5 Answers5

5

1.I've created next architecture
enter image description here
2.Added some constraints
enter image description here
3.In TableViewController I've added next code

import UIKit

class TableViewController: UITableViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        self.refreshControl = UIRefreshControl(frame: CGRectZero)
        self.refreshControl!.addTarget(self, action: "refresh:", forControlEvents: .ValueChanged)
    }

    func refresh(sender:UIRefreshControl)
    {
        self.refreshControl?.endRefreshing()
    }
}
  1. And uploaded example to github

IMPORTANT NOTE I've used Xcode 7 and Swift 2.

Alexey Bondarchuk
  • 2,002
  • 2
  • 20
  • 22
  • UITableViewController already has a refresh control. You just need to activate it for the view controller in the IB attribute inspector. So no need to add one. You just need to add the target action. Not sure how your suggestion fixes the issue, as I imagine @David has this code already in some form? – Rory McKinnel Sep 28 '15 at 14:39
  • @RoryMcKinnel Edited. This is the same behaviour :) And I guess @David adding `UIRefreshControl` as `subview` onto `UITableView`. Thats why jump bug appears. – Alexey Bondarchuk Sep 28 '15 at 14:44
  • Not sure if he adds one or not as no code shown. However if I create a project without adding one I still see the issue. I am pretty sure that the pause happens just as the value changed event is about to fire and the jump happens after it has fired due to continued scrolling by the user. BTW, saw you edited your post. You do not need to assign `self.refreshControl` either. Its done for you. – Rory McKinnel Sep 28 '15 at 15:01
  • @RoryMcKinnel Have you played with code provided by me? And still can reproduce the issue? – Alexey Bondarchuk Sep 28 '15 at 15:04
  • I think your code is only smooth because you have static cells rather than dynamic cells from prototypes. I stripped out all your code, set the table property to support refresh and just left your target action. Seems smooth, but I am pretty sure this is all down to the static content as there is no code left. I have the exact same project which jumps, but does have dynamic cell content. – Rory McKinnel Sep 28 '15 at 15:15
  • @RoryMcKinnel Also you can check [video](https://www.youtube.com/watch?v=I7PayvNWTC0&feature=youtu.be) and you'll see that I don't have jump issue – Alexey Bondarchuk Sep 28 '15 at 15:28
  • I figured out the issue. Seems you have to have margin relative constraints in your container view to recreate the issue. If you add these type of constraints to your code, it jumps as well. – Rory McKinnel Sep 28 '15 at 16:35
1

I managed to recreate your issue exactly by accident and managed to fix it, but at the cost of having no margins at all.

The jumping seems to happen if you use margin based constraints or any kind of margin for your container view. If you remove the margin relative part of the constraints, the jumping disappears.

Very strange, but seems to be the issue. As soon as I add any margin relative constraint for the container, the issue returns. Removing it and the display goes back to smooth scrolling.

This would seem to be a bug and I think you will need to raise a bug report with Apple.

Update:

Looking again, the issue seems to appear as soon as the container view is not the full width of the screen. Adding any sort of margin to the container view (via layout relative to margin or by setting a non zero offset on a constraint) results in the jumpy behavior.

Update:

Something would appear to be fundamentally broken with UITableView scrolling inside a container view which has any kind of margin. If you override the scrolling delegate, the content offset/bounds of the scroll view are being changed at the moment the refresh is about to trigger. Here is some debug showing the issue

Start pulling down:

Scroll bounds = {{0, -127.33333333333333}, {374, 423}}
Scroll pos = [0.000000,-127.333333]
Scroll bounds = {{0, -127.66666666666667}, {374, 423}}
Scroll pos = [0.000000,-127.666667]
Scroll bounds = {{0, -128.33333333333334}, {374, 423}}
Scroll pos = [0.000000,-128.333333]

Ok before here ------->

Activity spinner becomes fully populated. Jump in scroll position upwards.

Scroll bounds = {{0, -104}, {374, 423}}
Scroll pos = [0.000000,-104.000000]

Scroll position corrects itself

Scroll bounds = {{0, -128.33333333333334}, {374, 423}}
Scroll pos = [0.000000,-128.333333]
Scroll position jumps the other direction by the same amount
Scroll bounds = {{0, -151.33333333333334}, {374, 423}}
Scroll pos = [0.000000,-151.333333]

Value changed target action fires. Bounds seem to reset (think 44 is height of refresh control

Scroll bounds = {{0, -44}, {374, 423}}
Scroll pos = [0.000000,-44.000000]
Corrects back
Scroll bounds = {{0, -151.33333333333334}, {374, 423}}
Scroll pos = [0.000000,-151.333333]
Fully corrects to the right scroll position by jumping back.

Ok after here ------>

Scroll bounds = {{0, -128.66666666666666}, {374, 423}}
Scroll pos = [0.000000,-128.666667]
Scroll bounds = {{0, -129}, {374, 423}}
Scroll pos = [0.000000,-129.000000]
Scroll bounds = {{0, -129.33333333333334}, {374, 423}}
Scroll pos = [0.000000,-129.333333]
Scroll bounds = {{0, -129.66666666666666}, {374, 423}}
Scroll pos = [0.000000,-129.666667]
Scroll bounds = {{0, -130}, {374, 423}}

Conclusion

There seems to be no easy way I can find to work around this. I tried creating my own table view controller and the jumping goes away but is replaced by a different effect: that being that when you scroll down the top cell disappears, then reappears. I imagine it relates to the same internal issue, just being expressed differently.

Unfortunatley looks like you might have to put up with the effect or go for no margin. I would raise a bug report with Apple.

Only alternative option would be to create the margins in your UITableViewCells. You could make the cell content view have a clear background and introduce a left and right margin to your cells using an internal container view for your cell content. I think that may be you best chance.

And Finally...

Not to be defeated, you can apply a scaling transform to the navigation controller for the table view to create a margin doing the following in your table view controller:

override func viewWillAppear(animated: Bool) {
  super.viewWillAppear(animated)

  // Add a scaling transform to the whole embedded controller view.      
  self.navigationController!.view.transform=CGAffineTransformMakeScale(0.9, 0.9);
}

This makes the view for the embedded controller appear 90% smaller so it has a margin around the border. Change the scale to to change the border size.

Not ideal, but works perfectly with no jump scrolling and has a border. It also leaves you totally free to use rounded corners etc as the whole content is scaled.

Rory McKinnel
  • 7,936
  • 2
  • 17
  • 28
  • I could definitely see that as an issue, but honestly, I don't see how one can circumvent this especially when margin constraints are needed for this table. I can't simply rely on giving it a fixed height because given different screen sizes it won't look right. – David Sep 28 '15 at 16:35
  • Try creating the margin by setting the offset in your constraint yourself. I never use margins as they have caused me nothing but trouble. – Rory McKinnel Sep 28 '15 at 16:37
  • Not sure I quite understand. You mean I should programmatically add the constraint? Would you have an example? – David Sep 28 '15 at 16:39
  • If you want a margin of 8, set the offset as 8 in the constraint. In the constraints you either ping relative to the container or the margin. You can create a margin by just setting the offset relative to the container yourself. For different sizes you would have to set the values in code. Not ideal, but if it fixes the jumping... – Rory McKinnel Sep 28 '15 at 16:39
  • Wait, are you saying that I should not use my constraints that are like "Trailing Space to Superview equals -5. Leading Space to Superview equals -5", and instead I should programmatically edit the frame of my table? So something like if container is tableHolder: tableHolder.frame = CGRectMake(-5, viewToBeBelow.frame.origin.y + viewToBeBelow.frame.size.height + 7, self.view.frame.size.width - 10, self.view.frame.size.height - viewToBeBelow.frame.origin.y + viewToBeBelow.frame.size.height + 7)? I know that is a lot, but basically something like that might achieve what I want. – David Sep 28 '15 at 16:52
  • I mean where you have a constraint relative to the margin, simply make it relative to the container bay deselecting the margins option and adjust the offset to be whatever you want the margin be. If you want the margin to be settable in code, CTRL drag the top, bottom, leading and trailing constraints into your header and you can set the constant values in code to the margin offset you want. – Rory McKinnel Sep 28 '15 at 17:08
  • @David I have had another play with not using margins and creating my own margin. The result is that the issue seems to happen whenever the embedded table view is not the full screen width. Even putting in a wrapper view with a margin with the container view with no margin inside it produces the same issue. As soon as the table is not full screen width the issue appears. No solution sadly other than to have no margin at all at the moment. – Rory McKinnel Sep 28 '15 at 21:06
  • I awarded bounty to this, even though it is an ongoing investigation, lol. I still need to try some more things out, but it truly is a shame how much effort one needs to go through for this. – David Oct 02 '15 at 03:48
  • Final answer: Refresh Nav bar button on top right 8-) – Rory McKinnel Oct 02 '15 at 07:13
  • Haha, where appropriate. I use that in certain views. Sadly, doesn't pertain here. – David Oct 02 '15 at 07:16
  • Ugh, I'm experiencing something similar with a UIScrollView. Sadly I'm not using AutoLayout, I'm manually setting the frame, and I'm still getting this weird offset problem. – Shawn Throop Apr 30 '20 at 08:55
0

It seems that you've almost solved your problem (with a rough work around) using your off screen UIContainerView attempt. Give it another shot, but this time try:

  1. Increasing the row count within the numberOfRowsInSection: by 1.
  2. Inside your cellForRowAtIndexPath: method, set the last cell's rowHeight property to the distance your Container View is below the screen.

Step 2 won't work if you're using the method tableView:heightForRowAtIndexPath: - Instead, you'll need to set the height of the last cell using its index. Using this optional method can cause significant performance problems and can lead to lagged Refresh Controls too.

ChrisHaze
  • 2,800
  • 16
  • 20
0

Following up on my comment:

To get UIRefreshControl it to play nicely with a UICollectionView or UITableView I've tried many things, but in the end the UIRefreshControl really only works well in a UITableViewController.

Then there is also an issue with adjusting the tintColor of the UIRefreshControl: sometimes it colors the spinner, sometimes it doesn't, sometimes the tintColor needs to be set inside an animation-block for some reason to take effect.

So I gave up on UIRefreshControl, and implemented my own solution. It is not as simple as setting a UIRefreshControl on a UITableViewController, but:

  • it works perfectly (or at least I have not been able to find uncovered edge-cases: if you find them, please file a pull-request)

  • you can implement any kind of loading view (something that rotates, something that bounces, maybe even a map view, or some UIKit Dynamics).

You can find it here:

JRTRefreshControl

Naeem
  • 789
  • 1
  • 10
  • 23
Joride
  • 3,722
  • 18
  • 22
0

I've found cases where simply setting an estimatedRowHeight on the tableView to match the rowHeight resolves the glitch. For reference, my setup was a UITableViewController contained inside a UIViewController with a fixed rowHeight of 140.

Ben Packard
  • 26,102
  • 25
  • 102
  • 183