62

I put a UIScrollView in my nib's view, and linked it to a an IBOutlet property.

Now, when I do this in my viewDidLoad method, it seems to have no effect on the contentSize:

self.sv.backgroundColor = [UIColor yellowColor]; // this works
CGSize size =  CGSizeMake(1000.0, 1000.0); 
[self.sv setContentSize:size]; // this does not

It behaves as if the contentSize was the same as the frame. What's going on? This started working when I turned off AutoLayout. Why?

Cesare
  • 9,139
  • 16
  • 78
  • 130
cannyboy
  • 24,180
  • 40
  • 146
  • 252

15 Answers15

84

I had the same problem. Auto Layout for UIScrollView is messed up.

Work around: Put everything in the UIScrollView into another UIView, and put that UIView as the only child of the UIScrollView. Then you can use Auto Layout.

If things near the end is messed up (the end of whichever direction your UIScrollView scrolls), change the constraint at the end to have the lowest possible priority.

Cesare
  • 9,139
  • 16
  • 78
  • 130
yuf
  • 3,082
  • 3
  • 20
  • 18
  • 6
    @matt My answer actually uses the "Mixed approach:" way of configuring UIScrollView, as stated in apple's doc http://developer.apple.com/library/ios/#releasenotes/General/RN-iOSSDK-6_0/_index.html I tried the pure Autolayout way and couldn't get it to work for some reason. – yuf Nov 25 '12 at 23:00
  • 1
    The pure auto layout way isn't always appropriate, I agree. But it's way cool because the constraints of the subviews describe the content size, and you never set `contentSize` at all. Example here (everything generated in code): https://github.com/mattneub/Programming-iOS-Book-Examples/tree/master/ch20p570scrollViewInCode2 and here (everything created in nib): https://github.com/mattneub/Programming-iOS-Book-Examples/tree/master/ch20p573scrollViewAutoLayout2 – matt Nov 26 '12 at 00:19
  • @matt I agree it's cool, but I tried exactly what it said in that doc and it wasn't working for me. The contentView is really easy because it's what we were used to, and it's been working for me. I must be doing something that the pure autolayout approach doesn't support. – yuf Nov 26 '12 at 06:02
  • 1
    The link matt posted doesn't seem to have the information. Here is the correct link at the time I am posting this: https://developer.apple.com/library/ios/technotes/tn2154/_index.html – RajV Sep 09 '13 at 16:37
  • Try adding contentSize property in viewDidAppear() method – Deepak Singh Negi Oct 30 '13 at 08:35
  • I finally manage to make it work in X code 5 entirely in storyboard without using only one child view. It can have two children and still scroll. However I find it tricky to get right. For scrollview I set the leading, trailing, bottom space to bottom layout guide, top space to top layout guide (this one is tricky to get right, Xcode 5 almost always mistakenly make it **bottom** space to top layout guide if your view is maximized to take the whole window), and make them all 0. With Subviews I set spaces to all four edges to the container (the scroll view), and width and height as well. – huggie Nov 14 '13 at 06:21
  • Of course to lay it all out in IB I have to see it. And in simulated metrics I use free form and resize the direct child view of the controller (the superview of the scroll view) in the size inspector so I can see the whole "canvas" – huggie Nov 14 '13 at 06:23
  • @matt How do you limit `contentSize` with autolayout? I have `scrollView` with height depends on the screen size. If screen is 3.5" I'm trying to fit everything in smaller `scrollView`. I'm setting multiple constraints for subviews in scroll view like `@"V:|-(<=39@500,>=10@500)-[leftView(72)]-(>=3@500,<=20@500)-[loginButton(30)]-0-|"`, but the `contentSize` is the same for both `scrollView` heights. – derpoliuk Apr 08 '14 at 13:59
  • wow it worked like charm and of course i changed according to me +1 – Abi Sep 20 '15 at 05:02
  • And remember to layout that `contentView` manually in `layoutSubviews()` – onmyway133 Oct 25 '16 at 08:19
71

I tried viewWillLayoutSubviews to update scrollView's contentSize, it worked for me.

- (void)viewDidLayoutSubviews
{
  [self.bgScrollView setContentSize:CGSizeMake(320, self.view.frame.size.height* 1.5)];
}

Apple Doc

-(void)viewDidLayoutSubviews

Called to notify the view controller that its view has just laid out its subviews.

Discussion

When the bounds change for a view controller’s view, the view adjusts the positions of its subviews and then the system calls this method. However, this method being called does not indicate that the individual layouts of the view’s subviews have been adjusted. Each subview is responsible for adjusting its own layout.

Your view controller can override this method to make changes after the view lays out its subviews. The default implementation of this method does nothing.

HDdeveloper
  • 4,396
  • 6
  • 40
  • 65
  • You multiply by 1.5 the height of the view to set the scroll view height, but this will not work if one the controls inside the scrollview has a variable height. For instance, if you have a uiTextView with variable height (depending on its content) inside the scrollview, the scrollview height does not match its real height. – Jerome Jun 09 '16 at 10:37
  • @Jerome Its just an example to update the content size. You can set your own content size according to your need to fit the components. – HDdeveloper Jun 13 '16 at 05:12
  • You missed call to super: "[super viewDidLayoutSubviews];" – Uladzimir Dec 06 '16 at 19:23
21

The easiest/cleanest way is to set contentSize at viewDidAppear so you negate the effects of autolayout. This doesn't involve adding random views. However relying on load order for an implementation to work may not be the best idea.

Jlam
  • 1,932
  • 1
  • 21
  • 26
17

Use this code. ScrollView setContentSize should be called async in main thread.

Swift:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    DispatchQueue.main.async {
        var contentRect = CGRect.zero

        for view in self.scrollView.subviews {
           contentRect = contentRect.union(view.frame)
        }

        self.scrollView.contentSize = contentRect.size
    }
}

Objective C:

 - (void)viewDidLayoutSubviews {
     [super viewDidLayoutSubviews];

     dispatch_async(dispatch_get_main_queue(), ^ {
                        CGRect contentRect = CGRectZero;

                        for(UIView *view in scrollView.subviews)
                           contentRect = CGRectUnion(contentRect,view.frame);

                           scrollView.contentSize = contentRect.size;
                       });
}
Karen Hovhannisyan
  • 1,140
  • 2
  • 21
  • 31
  • This works perfectly for me, I already tested the almast solution on the web, but it didn't work as I was expecting – khalid Nov 28 '19 at 23:58
15

There are two problems here. (1) viewDidLoad is too soon; you have to wait until after layout has taken place. (2) If you want to use autolayout with a scrollview that comes from a nib, then either you must use constraints to completely describe the size of the contentSize (and then you don't set the contentSize in code at all), or, if you want to set it in code, you must prevent the constraints on the scrollview's subviews from dictating the contentSize. It sounds like you would like to do the latter. To do so, you need a UIView that acts as the sole top-level subview of the scrollview, and in code you must set it to not use autolayout, enabling its autoresizingMask and removing its other external constraints. I show an example of how to do that, here:

https://github.com/mattneub/Programming-iOS-Book-Examples/blob/master/ch20p573scrollViewAutoLayout/ch20p573scrollViewAutoLayout/ViewController.m

But notice also the next example, which shows how to use constraints completely, instead of contentSize.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • 1
    "you must use constraints to completely describe the size of the contentSize (and then you don't set the contentSize in code at all)" - This is the correct solution to the problem – Fervus Jul 22 '14 at 18:12
  • 1
    @Fervus except that in iOS 8, as seeded so far, it's broken. – matt Jul 22 '14 at 18:12
  • @matt, your link is broken. can you please restore it? – Samuel Dec 09 '16 at 00:33
  • @Samuel Examples for the current edition of my book are here: https://github.com/mattneub/Programming-iOS-Book-Examples The ones in question now start with "bk2ch07". – matt Dec 09 '16 at 02:47
15

A SUPER easy way to use AutoLayout with UIScrollViews inside Interface Builder:

Step 1: Create a UIScrollView

Step 2: Create a UIView that is a child of your scroll view like so:

-UIScrollView
---UIView
-----Your other content

(We'll call this one contentView).

Step 3: In the size inspector, give this view a height and width (say, 320x700).

Step 4 (using AutoLayout): Create unambiguous constraints from your contentView to its superview (the UIScrollView): connect the 4 edges (top, leading, trailing, bottom), then give it a defined width and height that you want it to scroll too.

For example: If your scroll view spans the entire screen, you could give your content view a width of [device width] and a height of 600; it will then set the content size of the UIScrollView to match.

OR:

Step 4 (not using AutoLayout): Connect both of these new controls to your view controller using IB (ctrl+drag from each control to your view controller's .h @implementation). Let's assume each is called scrollView and contentView, respectively. It should look like this:

@interface YourViewController : UIViewController

@property (strong, nonatomic) IBOutlet UIScrollView *scrollView;
@property (strong, nonatomic) IBOutlet UIView *contentView;

@end

Step 5 (not using AutoLayout): In the view controller's .h file add (actually, override) the following method:

-(void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];
    self.scrollView.contentSize = self.contentView.frame.size;
}
brandonscript
  • 68,675
  • 32
  • 163
  • 220
5

You can use this lines of code into your *.m file's

- (void)viewDidLoad{
   [scroll setContentSize:CGSizeMake(320, 800)]   ;
   [scroll setScrollEnabled:TRUE];
   [scroll setShowsVerticalScrollIndicator:NO];
   [scroll setShowsHorizontalScrollIndicator:YES];
}    

for this you need to take an IBOutlet property of UIScrollView into your *.h file this way:

IBOutlet UIScrollView *scroll;

And connect this from Storyboard.

Or,

You can use this method into your *.m file:

-(void)viewDidLayoutSubviews 
{
  [scroll setContentSize:CGSizeMake(320, self.view.frame.size.height* 1.5)];
   // this will pick height automatically from device's height and multiply it with 1.5  
}

This both solution works for me in xcode-5, xcode-6, xcode-6.1, xcode-6.2

imti
  • 1,283
  • 1
  • 11
  • 12
3

Setting the contentSize in viewDidAppear is critical.

But I also had a variation of what worked in the 3.5 inch screen, and the 4 inch screen. The 4 inch screen worked, the older one does not. Both iOS 7. Bizarre is an understatement!

PostCodeism
  • 1,070
  • 1
  • 12
  • 20
1

I could never get auto layout based on constraints to work. Since my view was already a subclass UIScrollView I solved it by overriding setContentView: and ignoring auto layouts zero height setContentSize: message.

@interface MyView : UIScrollView {}
@end

@implementation MyView
- (void)setContentSize:(CGSize)aSize {
    if (aSize.height > 0)
        [super setContentSize:aSize];
}
@end
Conor
  • 1,781
  • 17
  • 27
1

I used to do set up the uiscrollview programmatically UNTIL I watched the following wonderful tutorial, step by step how to get uiscrollview and uiview to work: https://www.youtube.com/watch?v=PgeNPRBrB18

After watching the video you will start liking Interface Builder I am sure.

Vote up

dev4life
  • 10,785
  • 6
  • 60
  • 73
1

Still not scrolling when dynamic height of labels exceeds view height.

I did what yuf's answer marked as correct above said to do (I added a content view to my scrollview and set the constraints leading, trailing, top bottom, and equal widths from the content view to the scroll view.) but still my view was not scrolling when the internal controls height exceeded the height of the scrollview.

enter image description here

Inside my content view I have an image and 3 labels below it. Each label adjusts their own height dependant on how much text is in them (they are set to word-wrap and numberoflines = 0 to achieve this).

The problem I had was my content view's height was not adjusting with the dynamic height of the labels when they exceeded the height of the scroll view/main view.

To fix this I worded out I needed to set the Bottom Space to Container constraint between my bottom label and the contentview and gave it a value of 40 (chosen arbitrarily to give it a nice margin at the bottom). This now means that my contentview adjusts its height so that there is a space between the bottom of the last label and itself and it scrolls perfectly!

Yay!

Guy Lowe
  • 2,115
  • 1
  • 27
  • 37
1

Try this out...
add all constraints like you do for UIView (See screenShot of my ViewControler in Storyboard)
enter image description here
Now trick begins. select your last object and select its bottom constraint. (See above screenShot, Instagram button's Bottom Constraint(Yellow line)) and Change the Constant in Size Inspector like in bellow screenshot.

enter image description here

i require Constant=8 but you can change as per your requirements. this Constant is the Space between That Orange Button's Bottom and the scrollView.

EDIT

Make Sure about your view's hierarchy .

0) ViewController.view (optional)
1) UIScrollView
2) UIView (Rename as "contentView")
3) UIView (this view is your content that will make scrollView scroll)

Anurag Sharma
  • 4,276
  • 2
  • 28
  • 44
Vatsal Shukla
  • 1,274
  • 12
  • 25
1

I finally worked out my own solution to this problem because in my case I couldn't use the view controller's life cycle. Create your own scroll view subclass and use it instead of UIScrollView. This even worked for a scroll view inside a collection view cell.

 class MyScrollView:UIScrollView {

    var myContentSize:CGSize = CGSize.zero  // you must set this yourself 

    override func layoutSubviews() {
       super.layoutSubviews()
       contentSize = myContentSize
    }

 }

My MyScrollView was defined in the nib with a tag of 90. If so this is a good way to set content size in the code in the parent view.

let scrollView = viewWithTag(90) as! MyScrollView      
scrollView.myContentSize = ...
0

If you are using AutoLayout a really easy way to set the contentSize of a UIScrollView is just to add something like this:

CGFloat contentWidth = YOUR_CONTENT_WIDTH;

NSLayoutConstraint *constraintWidth = 
 [NSLayoutConstraint constraintWithItem:self.scrollView 
                              attribute:NSLayoutAttributeTrailing 
                              relatedBy:NSLayoutRelationEqual 
                                 toItem:self.scrollView 
                              attribute:NSLayoutAttributeLeading 
                             multiplier:1 
                               constant:contentWidth];

[self.scrollView addConstraint:constraintWidth];
Alex Cio
  • 6,014
  • 5
  • 44
  • 74
korovyev
  • 34
  • 2
0

I got Autolayout to work for paginated scroll views whose pages occupy the full-width of the screen. The pages automatically resize according to the scroll view's size. I haven't tested this for lesser-width scroll views but do comment away if it works--I beleieve it should. Targeted for iOS 9, wrote code in Swift 2, used a mix of IB's and custom code in awakeFromNib.

Steps:

  • Define a full-screen scroll view.
  • Inside the scroll view, add a UIView (I called mine contentView) whose top, trailing, bottom, and leading edges to the scroll view are all zero; the height is equal to the scroll view's; but the width is the scroll view's width times the number of pages. If you're doing this visually, you will see your content view extend beyond your scroll view in Inteface Builder.
  • For every "page" inside the contentView, add Autolayout rules to put them side-by-side each other, but most importantly, give them each a constraint so that their widths are equal to the scroll view's, not the content view's.

Sample code below. embedChildViewController is just my convenience method for adding child VCs--do look at setupLayoutRulesForPages. I have exactly two pages so the function is too simple, but you can expand it to your needs.

In my view controller:

override func loadView() {
    self.view = self.customView
}

override func viewDidLoad() {
super.viewDidLoad()

self.embedChildViewController(self.addExpenseVC, toView: self.customView.contentView, fillSuperview: false)
self.embedChildViewController(self.addCategoryVC, toView: self.customView.contentView, fillSuperview: false)
self.customView.setupLayoutRulesForPages(self.addExpenseVC.view, secondPage: self.addCategoryVC.view)
}

My custom view:

class __AMVCView: UIView {

    @IBOutlet weak var scrollView: UIScrollView!
    @IBOutlet weak var contentView: UIView!
    @IBOutlet weak var pageControl: UIPageControl!

    override func awakeFromNib() {
        super.awakeFromNib()

        self.scrollView.pagingEnabled = true
        self.scrollView.bounces = true
        self.scrollView.showsHorizontalScrollIndicator = false
        self.scrollView.showsVerticalScrollIndicator = false

        self.pageControl.numberOfPages = 2

        self.contentView.backgroundColor = UIColor.blueColor()
        self.scrollView.backgroundColor = UIColor.clearColor()
        self.backgroundColor = UIColor.blackColor()
    }

    func setupLayoutRulesForPages(firstPage: UIView, secondPage: UIView) {
        guard self.contentView.subviews.contains(firstPage) && self.contentView.subviews.contains(secondPage)
            else {
                return
        }

        let rules = [
            "H:|-0-[firstPage]-0-[secondPage]-0-|",
            "V:|-0-[firstPage]-0-|",
            "V:|-0-[secondPage]-0-|"
        ]
        let views = [
            "firstPage" : firstPage,
            "secondPage" : secondPage
        ]
        let constraints = NSLayoutConstraint.constraintsWithVisualFormatArray(rules, metrics: nil, views: views)

        UIView.disableAutoresizingMasksInViews(firstPage, secondPage)
        self.addConstraints(constraints)

        // Add the width Autolayout rules to the pages.
        let widthConstraint = NSLayoutConstraint(item: firstPage, attribute: .Width, relatedBy: .Equal, toItem: self.scrollView, attribute: .Width, multiplier: 1, constant: 0)
        self.addConstraint(widthConstraint)
    }

}
Matthew Quiros
  • 13,385
  • 12
  • 87
  • 132