40

I have a fairly simple view configuration:

A UIViewController, with a child UIScrollView and a UIImageView in this UIScrollView. I set the UIImageView with a height sufficient to break out of the visible area (ie. higher to 1024pt), and set the Bottom space to superview constraint of my UIImageView to a fixed positive value (20 for example).

project layout

The whole setup works as expected, the image scrolls nicely in its parent. Except when the view is scrolled (the effect is more visible if you scrolled to the bottom of the view), then disappear, and appear again (you switched to another view and came back) the scrolling value is restored, but the content of the scroll view is moved to the outside top part of its parent view.

This is not simple to explain, I'll try to draw it: visual representation of previous paragraph

If you want to test/view the source (or the storyboard, I did not edit a single line of code). I put a little demo on my github: https://github.com/guillaume-algis/iOSAutoLayoutScrollView

I did read the iOS 6 changelog and the explanation on this particular topic, and think this is the correct implementation of the second option (pure auto layout), but in this case why is the UIScrollView behaving so erratically ? Am I missing something ?

EDIT: This is the exact same issue as #12580434 uiscrollview-autolayout-issue. The answers are just workarounds, as anyone found a proper way to fix this or is this a iOS bug ?

EDIT 2: I found another workaround, which keep the scroll position in the same state the user left it (this is an improvement over 12580434's accepted answer):

@interface GAViewController ()

@property CGPoint tempContentOffset;

@end


@implementation GAViewController

-(void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    self.tempContentOffset = self.mainScrollView.contentOffset;
    self.scrollView.contentOffset = CGPointZero;
}

-(void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    self.scrollView.contentOffset = self.tempContentOffset;
}

This basically save the offset in viewWillAppear, reset it to the origin, and then restore the value in viewDidAppear. The problem seems to occur between these two calls, but I can't find its origin.

Community
  • 1
  • 1
Guillaume Algis
  • 10,705
  • 6
  • 44
  • 72
  • 1
    Thanks for your second edit, it seems to the best work around I can find. The solution of Mark Kryzhanouski does not work in my setup. The only problem with EDIT 2 is that, when navigating back, you can see scrollview offset snap back to its original position. – thijsai Mar 23 '13 at 14:23
  • In my case I was able to work around the issue by adding all contraints to the top level view, rather than the scroll view. – railwayparade May 31 '13 at 03:03
  • Brilliant explanation of this issue. I am running into it now and I can't believe it hasn't been fixed. – chourobin Jun 26 '13 at 21:28
  • You should really write your EDIT 2 into an answer so that we can upvote it. – Pang Feb 06 '14 at 08:12
  • @Pang Upvote the question ;). I'm not adding it as an answer because it's just a workaround. And afaik it was fixed in iOS 7. – Guillaume Algis Feb 06 '14 at 09:02

7 Answers7

22

Yeah, something strange happened with UIScrollView in pure autolayout environment. Re-reading the iOS SDK 6.0 release notes for the twentieth time I found that:

Note that you can make a subview of the scroll view appear to float (not scroll) over the other scrolling content by creating constraints between the view and a view outside the scroll view’s subtree, such as the scroll view’s superview.

Solution

Connect your subview to the outer view. In another words, to the view in which scrollview is embedded.

As IB does not allow us set up constraints between the imageView and a view outside the scroll view’s subtree, such as the scroll view’s superview then I've done it in code.

- (void)viewDidLoad {
    [super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
    [self.view removeConstraints:[self.view constraints]];
    [self.scrollView removeConstraints:[self.scrollView constraints]];
    [self.imageView removeConstraints:[self.imageView constraints]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|[_scrollView]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_scrollView)]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_scrollView]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_scrollView)]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|[_imageView(700)]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_imageView)]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_imageView(1500)]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_imageView)]];
}

And vau! It works!

Mark Kryzhanouski
  • 7,251
  • 2
  • 22
  • 22
  • 4
    Oh. It works. But why ? It seems to me that it's a sort of blending of the two options Apple provides... And to me, the info you quoted explains how to make a view with a sort of "CSS absolute positioning", which is not supposed to scroll at all. – Guillaume Algis Mar 12 '13 at 09:45
  • 1
    I can't explain it too. Seems this is a sort of UIScrollView bug. Hope Apple will change this in future. – Mark Kryzhanouski Mar 12 '13 at 10:36
  • Oh well, strange as it seems you're saying "the UIImageView should always stick to the left side of it's parent view" and this constraint is being held by the controller's view. You're not really saying that the UIImageView should always stick to the left side of the controller's view. – Fábio Oliveira Jul 23 '13 at 17:35
  • What does `[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|[_imageView(700)]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_imageView)]];` mean exactly? Does `|` here mean its superview (i.e. the scroll view) or does it refer to self.view? – Hari Honor Aug 28 '13 at 10:16
  • It means second option (self.view). It is view in which scrollview is embedded. – Mark Kryzhanouski Aug 29 '13 at 10:09
  • I still cannot understand the solution, however I notice that when the navigationController pops back to the scrollView's NC, there will be a breaking constraint error. While I cannot encounter the bug in iOS 7 anymore, I still need those mystery codes for iOS 6 compatibility.... :/ – vk.edward.li Dec 12 '13 at 12:26
10

The edit didn't work for me. But this worked:

-(void)viewWillDisappear:(BOOL)animated
{
     [super viewWillDisappear:animated];

     self.tempContentOffset = self.scrollView.contentOffset;
     self.scrollView.contentOffset = CGPointZero;
}

- (void)viewDidLayoutSubviews {
     [super viewDidLayoutSubviews];
     self.scrollView.contentOffset = self.tempContentOffset;
}
Pion
  • 708
  • 9
  • 21
5

For me I went to the IB clicked my view controller that contains the scroll view. Then I went to Attribute Inspector -> View Controller -> Extend Edges -> Uncheck "Under Top Bars" and "Under Bottom Bars".

Andy
  • 474
  • 3
  • 6
2

Simple solution found, Just put

[self setAutomaticallyAdjustsScrollViewInsets:NO];

in your ViewControllers viewDidLoad method

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.

    [self setAutomaticallyAdjustsScrollViewInsets:NO];
}
Purushottam Sain
  • 306
  • 2
  • 10
0

I had a similar problem using a UIScrollView in a UIViewController with a top extra space that was not visible in Storyboard. The solution for me was to uncheck the "Adjust Scroll View Insets" on the ViewController storyboard properties : see answer Extra Space (inset) in Scroll View at run time

Maxime T
  • 848
  • 1
  • 9
  • 17
  • Please read the question and answers carefully. This is not the same problem. This was a bug in iOS 6, before iOS 7 was even announced. – Guillaume Algis Oct 27 '14 at 09:23
  • 1
    Issue he is describing is still present in iOS 8, it wasn't fixed or it was reintroduced again (if it's indeed bug and not a feature, you never know with Apple). While it's not really related to the question, it helped me to solve my problem, so thanks for that :) – Lope Nov 20 '14 at 05:42
-2

Add a global property contentOffset and save the current contentOffset in viewDidDisappear. Once you return the method viewDidLayoutSubviews will be called and you can set your original contentOffset.

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];
    [self.scrollView setContentOffset:self.contentOffset animated:FALSE];
}

- (void)viewDidDisappear:(BOOL)animated
{
    [super viewDidDisappear:animated];
    self.contentOffset = self.scrollView.contentOffset;
    [self.scrollView setContentOffset:CGPointMake(0, 0) animated:FALSE];
}
  • 3
    1. This question was already solved nearly a year ago. 2. This is bad, because `viewDidLayoutSubviews` will probably be called again later during the controller lifecycle and this will reset the contentOffset again (which is not wanted). 3. Use `NO` instead of `FALSE` in Objective-C. 4. `CGPointMake(0,0)` => `CGPointZero`. – Guillaume Algis Feb 28 '14 at 15:27
-3

Looks like the problem solved with the dispatch_async during the viewWillAppear:

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    CGPoint originalContentOffset = self.scrollView.contentOffset;
    self.scrollView.contentOffset = CGPointZero;

    dispatch_async(dispatch_get_main_queue(), ^{
        self.scrollView.contentOffset = originalContentOffset;
    });
}
chebur
  • 614
  • 8
  • 16
  • 1
    Why is this downvoted? It actually fixed the bug for me, and it works without flickering, and no need to recreate view constraints in code. – pckill May 13 '14 at 12:38
  • 1
    I don't know why this is downvoted. I got the similar issue, then need call this self.contentInset = UIEdgeInsetsZero and then call self.contentInset = insets; again in main queue. All value I logged are correct but the UI still wrong. Thank Chebur. – huyleit Oct 23 '14 at 02:17