UIScrollView adjusts contentOffset when contentSize changes
Asked Answered
W

8

43

I am adjusting a detail view controller's state, just before it is pushed on a navigationController:

[self.detailViewController detailsForObject:someObject];
[self.navigationController pushViewController:self.detailViewController
                                     animated:YES];

In the DetailViewController a scrollView resides. Which content I resize based on the passed object:

- (void)detailsForObject:(id)someObject {
    // set some textView's content here
    self.contentView.frame = <rect with new calculated size>;

    self.scrollView.contentSize = self.contentView.frame.size;
    self.scrollView.contentOffset = CGPointZero;
}

Now, this all works, but the scrollView adjusts it's contentOffset during the navigationController's slide-in animation. The contentOffset will be set to the difference between the last contentSize and the new calculated one. This means that the second time you open the detailsView, the details will scroll to some unwanted location. Even though I'm setting the contentOffset to CGPointZero explicitly.

I found that resetting the contentOffset in - viewWillAppear has no effect. The best I could come up with is resetting the contentOffset in viewDidAppear, causing a noticeable up and down movement of the content:

- (void)viewDidAppear:(BOOL)animated {
    self.scrollView.contentOffset = CGPointZero;
}

Is there a way to prevent a UIScrollView from adjusting its contentOffset when its contentSize is changed?

Watts answered 29/6, 2011 at 15:29 Comment(4)
Why are you adjusting the content offset within an animation block? Skip the CATransaction stuff, so it doesn't animate. Then it won't look choppy.Petrina
Removing the CATransaction stuff indeed does not change a thing, so it is removed.Watts
Are you sure viewDidAppear is getting called? I'd set a breakpoint or a NSLog statement in it. Also, have you tried switching the order of your calls, i.e. setting the detailsForObject after pushing?Petrina
Yes it is called, and it set's the correct contentOffset to the scrollView. But the contentOffset animation occurs before viewDidAppear is called.Watts
H
62

Occurs when pushing a UIViewController containing a UIScrollView using a UINavigationController.

iOS 11+

Solution 1 (Swift Code):

scrollView.contentInsetAdjustmentBehavior = .never

Solution 2 (Storyboard)

enter image description here

iOS 7

Solution 1 (Code)

Set @property(nonatomic, assign) BOOL automaticallyAdjustsScrollViewInsets to NO.

Solution 2 (Storyboard)

Uncheck the Adjust Scroll View Insets

Adjust Scroll View Insets

iOS 6

Solution (Code)

Set the UIScrollView's property contentOffset and contentInset in viewWillLayoutSubviews. Sample code:

- (void)viewWillLayoutSubviews{
  [super viewWillLayoutSubviews];
  self.scrollView.contentOffset = CGPointZero;
  self.scrollView.contentInset = UIEdgeInsetsZero;
}
Halloran answered 2/12, 2013 at 10:7 Comment(3)
This is a new way of solving the problem. It is more neat then resetting the contentSize.Watts
Outstanding answer. Thanks for posting the differences in iOS 7 and even including the images to clarify. Super!Circe
Glad to have helped you guys :)Halloran
W
15

The cause of this problem remains unclear, though I've found a solution. By resetting the content size and offset before adjusting them, the UIScrollView won't animate:

- (void)detailsForObject:(id)someObject {
    // These 2 lines solve the issue:
    self.scrollView.contentSize = CGSizeZero;
    self.scrollView.contentOffset = CGPointZero;

    // set some textView's content here
    self.contentView.frame = <rect with new calculated size>;

    self.scrollView.contentSize = self.contentView.frame.size;
    self.scrollView.contentOffset = CGPointZero;
}
Watts answered 11/7, 2011 at 13:45 Comment(0)
H
3

I had the same issue with a UIScrollview, where the problem was caused by not setting the contentSize. After setting the contentSize to the number of items this problem was solved.

self.headerScrollView.mainScrollview.contentSize = CGSizeMake(320 * self.sortedMaterial.count, 0);
Huron answered 23/9, 2013 at 13:29 Comment(0)
S
3

Here's what worked for me:
In the storyboard, in the Size Inspector for the scrollView, set Content Insets Adjustment Behavior to "Never".

enter image description here

Shelbyshelden answered 13/7, 2018 at 20:54 Comment(0)
E
0

Is your scrollView the root view of the DetailViewController? If yes, try wrapping the scrollView in a plain UIView and make the latter the root view of DetailViewController. Since UIViews don't have a contentOffset property, they are immune to content offset adjustments made by the navigation controller (due to the navigation bar, etc.).

Electronics answered 29/6, 2011 at 16:21 Comment(1)
The scrollView is not the root view of the DetailViewController. self.scrollView is bound to a UIScrollView that resides in the DetailViewController's root view.Watts
C
0

I experienced the problem, and for a specific case - I don't adjust the size - I used the following:

float position = 100.0;//for example

SmallScroll.center = CGPointMake(position + SmallScroll.frame.size.width / 2.0, SmallScroll.center.y);

Same would work with y: anotherPosition + SmallScroll.frame.size.height / 2.0

So if you don't need to resize, this is a quick and painless solution.

Creech answered 16/8, 2012 at 13:29 Comment(0)
R
0

I was experiencing a similar problem, where UIKit was setting the contentOffset of my scrollView during push animations.

None of these solutions were working for me, maybe because I was supporting iOS 10 and iOS 11.

I was able to fix my issue by subclassing my scrollview to keep UIKit from changing my offsets after the scrollview had been removed from the window:

/// A Scrollview that only allows the contentOffset to change while it is in the window hierarchy. This can keep UIKit from resetting the `contentOffset` during transitions, etc.
class LockingScrollView: UIScrollView {
    override var contentOffset: CGPoint {
        get {
            return super.contentOffset
        }
        set {
            if window != nil {
                super.contentOffset = newValue
            }
        }
    }
}
Roughhew answered 17/7, 2018 at 18:52 Comment(3)
That is a pretty rough solution. You might run into problems with iOS updates etc. Could you debug more and see what is causing UIKit to misbehave in your case? (when is the setter called? are there settings on the UI that work? do you have other animations running? do you have custom transitions, etc?)Watts
This is no doubt a heavy-handed approach, and I would prefer something else. I've done a bit more digging and found that the setter is called as a result of the view's _didMoveFromWindow:toWindow. It looks like this is a related issue: #13729859Roughhew
I have updated my answer with a slightly cleaner (although still heavy-handed) approach.Roughhew
C
0

Adding to KarenAnne's answer:

iOS 11+

automaticallyAdjustsScrollViewInsets was deprecated

Use this istead:

Storyboards:

enter image description here

Code (Swift):

scrollView.contentInsetAdjustmentBehavior = .never
Crematorium answered 24/5, 2021 at 15:27 Comment(2)
I think it would be better if you propose this as an edit to KarenAnne's answer.Endospore
You are right @Yeheshuah. Sorry I wasn't aware I could propose an edit.Crematorium

© 2022 - 2024 — McMap. All rights reserved.