UIScrollView's origin changes after popping back to the UIViewController
Asked Answered
B

17

58

I have a UIViewController subclass as a scene in the storyboard that contains a UIScrollView containing various subviews. One of the subviews is a UIButton which segues into another scene UIViewController subclass. When I come back from the view (pop the UIViewController off the navigation controller stack), I find that the scroll view's origin has somehow changed, although the contentsize and contentoffset seem correct.

What's also interesting is that the app has a tab bar, and when I tab away and back to that view, the scroll view is set back correctly with offset at (0, 0).

There is basically no code involved in this process, as it's pretty much all in the storyboard. As I am fairly new to using the storyboard, I figure I'm doing something wrong, although I don't know what. Any ideas as to what that may be? Perhaps sizing issues or constraints?

Brightness answered 1/7, 2013 at 12:38 Comment(5)
I've got a similar problem with UICollectionView while presenting a VC modally, still didn't solvedBuckram
Have you tried to set the origin in viewWillAppear? That is where the controller returns from a pop. It wont see viewDidLoad the second time (or third . . .) it loads.Buchheim
Well, I tried the following in viewWillAppear: self.scrollView.frame = CGRectMake(self.scrollView.frame.origin.x, self.scrollView.frame.origin.y - y, self.scrollView.frame.size.width, self.scrollView.frame.size.height); (where y is the new "origin" height), but it seems to have no effect. Is there another way to set the origin?Brightness
Are you using auto-layout in your storyboard? If so, setting the scrollview's frame will not work. You need to set the relevant constraint instead. In your case, perhaps try pinning the top of the scrollview to its containing view.Cotter
Steph, thanks for you response. I tried pinning the scrollview as you said, but there are already so many "top space" constraints for the scrollview that must have been auto-generated (over 100!). I assume I would have to delete them or at least lower their priority, which seemed rather painful. In the end, I used a variation of the answer in #12580934.Brightness
B
12

Actually, I put that line of code in viewDidDisappear, and so that it remembers the offset when the view reappears, I added this line before it

 self.contentOffset = self.scrollView.contentOffset;

as well as

 - (void)viewDidLayoutSubviews { 
       self.scrollView.contentOffset = self.contentOffset; 
 }
Brightness answered 10/7, 2013 at 15:22 Comment(0)
F
92

In iOS 7/8/9 simple self.automaticallyAdjustsScrollViewInsets = NO; solved the problem in my case.

Fanchan answered 6/2, 2014 at 7:42 Comment(8)
Simple solution! I just found this by searching and solved my problem.Chabazite
excellent. I have used it when switching between tab bar's tab.Design
A side note: suppose you have a TabBarController that contains a UIViewController that contains a ContainerController, this like should be added to your TabBarController.Gustie
for me setting it to self.automaticallyAdjustsScrollViewInsets = YES; solved my problem in iOS8Masseur
Another problem that should NOT even happen if Cocoa was more decentDisfigure
I think because of autolayout constraints it tries to adjust with some insets. But ultimately ends up spoiling the layout. Thanks, it worked :)Anjelicaanjou
It worked for another situation as well. While navigating from UICollectionViewController to a UIPagingViewController the y position of the view was shifting. This line of code solved it.Fimbriate
you saved me dude!Ferri
E
31

Try this in viewWillAppear of the view controller you pop back into:

 self.scrollView.contentOffset = CGPointMake(0, 0);

Edit: When also adding Peter's code you get the best results with:

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:YES];
    self.scrollView.contentOffset = CGPointMake(0, 0);
}

plus

- (void)viewWillDisappear:(BOOL)animated {  
    self.recentContentOffset = self.scrollView.contentOffset;
    [super viewWillDisappear:animated];
}

and

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];
    self.scrollView.contentOffset = CGPointMake(0, self.recentContentOffset.y);
}

You return to the original scroll position, have no visual side-effect and the scroll view itself is correctly positioned in its superview (which was a problem) after coming back.

Electrician answered 10/7, 2013 at 11:29 Comment(2)
I have put the code of viewDidLayoutSubviews in viewWillAppear, since viewDidLayoutSubviews is not called after a pop. But thanks for the answer!Scorecard
Ian Wilkinson solves it by subclassing UIScrollView, which I think is brilliant https://mcmap.net/q/331599/-frame-doesn-39-t-reflect-auto-layout-constraints-after-dismissing-modal-view-controllerDaggett
B
12

Actually, I put that line of code in viewDidDisappear, and so that it remembers the offset when the view reappears, I added this line before it

 self.contentOffset = self.scrollView.contentOffset;

as well as

 - (void)viewDidLayoutSubviews { 
       self.scrollView.contentOffset = self.contentOffset; 
 }
Brightness answered 10/7, 2013 at 15:22 Comment(0)
D
6

i had a similar problem, after dismissing a viewController, the contentOffset from my tableView was changed to (0, -64).

my solution was a little weird, i tried all the other answers but had no success, the only thing that fixed my problem was to switch the tableView position in the controls tree of the .xib

it was the first control in the parent View like this:

before

I moved the tableView right after the ImageView and it worked:

after

it seems that putting the table view in the first position was causing the trouble, and moving the table view to another position fixed the problem.

P.D. I'm not using autoLayout neither storyboards

hope this can help someone!

Deas answered 11/4, 2014 at 17:19 Comment(1)
This worked for me WITH storyboard & auto layout... I just put some transparent dummy view under everything else with frame {0,0,0,0} and it worked!Uriel
D
5

I'm using a collectionView and I had a similar problem. For iOS 11: in the size inspector, there is "content inset". Set that to "Never". That solved the problem for me. I hope this helps someone.

Objective C:

if (@available(iOS 11, *)) {
   [UIScrollView appearance].contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}

Swift:

if #available(iOS 11, *) {
UIScrollView.appearance().contentInsetAdjustmentBehavior = .never
}
Decern answered 27/9, 2017 at 0:54 Comment(8)
The only one that helped. Thanks!Weightlessness
No problem. Glad I helped!Decern
This should be the accepted answer when working with iOS 11Jonijonie
After trying several of the answers here, this one worked. Xcode 9, iOS 11. Thank you!Belter
@JosephFrancis did you have crashes with using this?Remediosremedy
@Remediosremedy No, I didn't have any crashes. Are you having any?Decern
yes, actually only in my error reporting tool, I see the following crashes "-[_UIAppearance setContentInsetAdjustmentBehavior:]: unrecognized selector sent to instance", But when I run on xcode it works perfect, I dont know why, I am not able to replicate it.Remediosremedy
@Remediosremedy I'm not sure either; try doing in storyboard. See if that works.Decern
S
4

In iOS 11, I faced a similar issue where when I come back after popping a view controller, the tableview in the previous viewcontroller used to adjust its content inset automatically which resulted in tableview contents jumping from top abruptly. The following solution worked for me in iOS 11.

In Storyboard, select the tableview and go to Attributes inspector and uncheck "Automatic" for Row Height and Estimate fields. Also change the content insets from "Automatic" to "Never".

[Table view]

Sectary answered 9/1, 2018 at 12:27 Comment(0)
T
0

Unfortunately, Peter and MacMark's suggestions did not work for me (Xcode 5 w/ auto-layout). The solution was to go to the storyboard, select the view controller, and Reset to Suggested Constraints in View Controller.

enter image description here

Therapy answered 26/9, 2013 at 8:5 Comment(2)
So what are the suggested constraints ?Olfactory
@pe60t0 disregard this answer, it worked around the problem rather than fixing it. Alex's answer is correct.Therapy
K
0

I posted a question recently about a similar issue but in a different context: Frame doesn't reflect auto layout constraints after dismissing modal view controller

In my case, the origin of a custom container view inside the scroll view is displaced, rather than the origin of the scroll view itself. With respect to auto layout, the custom container view is pinned to the four sides of the scroll view.

The container view is created and configured programmatically rather than in IB. Although the constraints of the container view are unchanged, the container view's origin is displaced after dismissing a modal view controller. This disconnect between constraints and frames is inexplicable.

What's even more remarkable is that the origin displacement problem remained after I removed and re-added all related constraints.

I came up with a similar solution: save the value for the current content offset, set the content offset to "zero", let the system swap out your views, then restore the content offset (i.e., a content-offset dance).

In my case, however, I had to take additional steps to resolve the issue. My scroll view is a paging scroll view that gets its subviews from a tilePages method (demoed in an old WWDC video). Changing the scroll view's content offset triggers the tilePages method via the UIScrollViewDelegate's scrollViewDidScroll: method. I had to set a flag to turn off tilePages while I did the content-offset dance, otherwise the app would crash.

Hopefully, I can remove the code for the content-offset dance when I upgrade to iOS 7.

Kinsley answered 27/9, 2013 at 22:47 Comment(0)
Z
0

Continued Issue when following the current answers:

The second attempt to open the presented view controller, without having left the presenting view controller, the problem remained. Which is why I am posting the exact steps that resulted in my solution.

  1. So, I reset the collectionView's constraints in the Storyboard, making certain they were pinned to presentingViewController's main view.

  2. Added: self.view.translatesAutoresizingMaskIntoConstraints = YES; inside the viewDidLoad of the presenting view controller.

  3. And stored, privately, the contentOffset of the collection view prior to the modally presented view controller's appearance:

    (void)viewWillDisappear:(BOOL)animated
    {
        [super viewWillDisappear:animated];
    
        self.contentOffset = self.collectionView.contentOffset;
        self.collectionView.contentOffset = CGPointZero;
    }
    
    - (void)viewDidLayoutSubviews {
         [super viewDidLayoutSubviews];
         self.collectionView.contentOffset = self.contentOffset;
    }
    
Zamboanga answered 28/1, 2015 at 23:51 Comment(2)
Hi, viewDidLayoutSubviews did not call when I didmiss presented view controller with overcurrentcontextTruditrudie
@Truditrudie if you scroll down to the newer responses on this #11236867 it might help.Zamboanga
B
0

recently , I have encountered this bug, can be solved using these codes:

// fix ios 6 bug begin
- (void)viewDidDisappear:(BOOL)animated
{
    [super viewDidDisappear:animated];

    if ([[[UIDevice currentDevice] systemVersion] floatValue] < 7) {
        isRecoredforios6 = YES;
        recordedOffsetforios6 = self.tableView.contentOffset;
        recordedSizeforios6 = self.tableView.contentSize;
    }
}

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];
    if (isRecoredforios6) {
        isRecoredforios6 = NO;
        self.tableView.contentSize = recordedSizeforios6;
        self.tableView.contentOffset = recordedOffsetforios6;
    }
}
// fix ios 6 bug end

thanks Peter Jacobs!

Barimah answered 8/9, 2015 at 4:17 Comment(0)
I
0
- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];

    if ([[[UIDevice currentDevice] systemVersion] floatValue] < 7) {
        _isRecoredforios6 = YES;
        _recordedOffsetforios6 = _scrollView.contentOffset;
        _recordedSizeforios6 = _scrollView.contentSize;
        _scrollView.contentOffset = CGPointZero;
    }

}

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

    if (_isRecoredforios6) {
        _isRecoredforios6 = NO;
        _scrollView.contentSize = _recordedSizeforios6;
        _scrollView.contentOffset = _recordedOffsetforios6;
    }
}

I have fixed this ios6 bug , can be solved using these codes. I solved bug occurred in Scrollview. Thank above all my friends!

Insubstantial answered 26/2, 2016 at 11:46 Comment(0)
P
0

After trying lots of things:

  • Set automaticallyAdjustsScrollViewInsets to false to see if the problem was with the navigation bar.
  • Playing with fixed cell heights...
  • Caching the cell heights...
  • Keeping a property with the table view offset, caching it and setting it on viewWillAppear...
  • etc.

I realised the issue was about the estimatedRowHeight value, which was set to 50px, when it should be ~150px. Updating the value fixed the issue and now the table view keeps the same offset.

Piercing answered 22/5, 2017 at 15:11 Comment(0)
R
0

Have you tried checking the Extend Edges Under Opaque Bars option? It can be found in your controller attributes inspector inside the storyboard.

It did the trick for me using XCode 9 iOS 11.

enter image description here

Revolver answered 14/11, 2017 at 16:20 Comment(0)
C
0

None of the above worked for me, I managed to do my own custom Push/Pull animation instead and it works like a charm.

First, add this class which implements Push scenario

class PushAnimator: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {

func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
    return 0.5
}

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

    // get reference to our fromView, toView and the container view that we should perform the transition in
    let container = transitionContext.containerView
    let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from)!
    let toView = transitionContext.view(forKey: UITransitionContextViewKey.to)!

    // start the toView to the right of the screen
    var frame = toView.frame
    frame.origin.x = container.frame.width
    toView.frame = frame

    // add the both views to our view controller
    container.addSubview(toView)
    container.addSubview(fromView)

    // get the duration of the animation
    let duration = self.transitionDuration(using: transitionContext)

    // perform the animation!
    UIView.animate(withDuration: duration, animations: {

        var frame = fromView.frame
        frame.origin.x = -container.frame.width
        fromView.frame = frame

        toView.frame = container.bounds

    }, completion: { _ in
        // tell our transitionContext object that we've finished animating
        transitionContext.completeTransition(true)

    })
}
}

Then add this class which implements pop scenario

import Foundation
import UIKit

class PopAnimator: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {

func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
    return 0.5
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
    // get reference to our fromView, toView and the container view that we should perform the transition in
    let container = transitionContext.containerView
    let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from)!
    let toView = transitionContext.view(forKey: UITransitionContextViewKey.to)!

    // set up from 2D transforms that we'll use in the animation
    let offScreenRight = CGAffineTransform(translationX: container.frame.width, y: 0)

    // start the toView to the right of the screen
    var frame = toView.frame
    frame.origin.x = -container.frame.width
    toView.frame = frame

    // add the both views to our view controller
    container.addSubview(toView)
    container.addSubview(fromView)

    // get the duration of the animation
    let duration = self.transitionDuration(using: transitionContext)

    // perform the animation!
    UIView.animate(withDuration: duration, animations: {

        fromView.transform = offScreenRight
        toView.frame = container.bounds

    }, completion: { _ in
        // tell our transitionContext object that we've finished animating
        transitionContext.completeTransition(true)

    })
}
}

Then add this line to your viewDidLoad method to change default NavigationController Delegate

self.navigationController?.delegate = self

And see the magic :)

Cellobiose answered 12/1, 2018 at 4:29 Comment(0)
E
0

I used a combination of the different solutions posted everywhere on SO, and came up with this subclass:

// Keeps track of the recent content offset to be able to restore the
// scroll position when a modal viewcontroller is dismissed
class ScrollViewWithPersistentScrollPosition: UIScrollView {

    // The recent content offset for restoration.
    private var recentContentOffset: CGPoint = CGPoint(x: 0, y: 0)

    override func willMove(toWindow newWindow: UIWindow?) {
        if newWindow != nil {
            // save the scroll offset.
            self.recentContentOffset = self.contentOffset
        }
        super.willMove(toWindow: newWindow)
    }

    override func didMoveToWindow() {
        if self.window != nil {
            // restore the offset.
            DispatchQueue.main.async(execute: {
                // restore it
                self.contentOffset = self.recentContentOffset
            })
        }
        super.didMoveToWindow()
    }
 }

Tested on iOS 11.2 / Xcode 9.2.

Erma answered 14/3, 2018 at 20:58 Comment(0)
W
0

Just in case none of the above helped, I had this issue as well as the issue for me was that i had the following code..

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    tableView.reloadData()
}

Commenting out this reloadData() line fixed the issue for me. I didn't need this line so not sure why i even added it!

Hopefully this solution helps someone else.

Walloon answered 6/3, 2019 at 16:45 Comment(0)
H
-9

Turn off AutoLayout option for that xib otherwise.

Hord answered 26/8, 2014 at 9:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.