iOS Programming Recipe 36: A Fixed Width Dynamic Height ScrollView in AutoLayout

As an iOS Developer it seems I come across a common situation where I want to add a content view with the same width as the screen inside a scroll view. Since most apps need to have responsive layouts, this usually makes sense to do using Autolayout. At first when learning about scrollviews, they seemed like magic. Due to their slightly different rules they were also a bit frustrating.

Assumptions

  • You are a fairly comfortable with XCode and interface builder.

The Content View Within The ScrollView

Ok, as this tutorial focuses primarily on autolayout. I want to show you how to do this in all forms. The first form will be interface builder as this is is the most visual.

Setting up Vertical ScrollView inside interface builder

Start by creating a new single view controller application, open the storyboard, and add a scrollview to the view controller already added in the provided storyboard. Make the scrollview fill up the entire view controller view. Then using the pin menu in the bottom right hand corner of the storyboard add a top, left, right, and bottom constraint to the scrollView. Make sure you uncheck the “constrain to margin” checkbox. Figure 1 shows the pin menu with correct settings. This will add constraints between the scrollview and the super view.

Figure 1

Figure 1 : Adding top, left, right, and bottom constraints to the scrollView.

Now you’ll want to add a standard view inside of the UIScrollView. As you did before add a top, left, bottom,and right constraint to the new view. Make sure all of these constraints also have a value of 0.

When You’re done. You can see the new constraints for both the scrollView and the Container view by selecting the view from the view pane and looking at the Size inspector on the right. Figure 2 two shows the container view, but the scroll view should look identical

Figure2a

Figure 2: The size inspector showing the constraints on the scrollView and the container view

Ok. Now we have a container view inside of our content view. One would think that because we have top , left, bottom, and right constraints pinned to the edge of the scrollview, then the container view should always be at the top of the scrollview and as wide as the window. This is not true however, because scrollviews are a little different. These constraints define the content size on a scrollview, and because our view doesn’t have a width or height. That content size is 0 wide and 0 tall.

Usually we want a scrollview to only scroll vertically, so we don’t want the container view to ever be wider than the window. The height we’ll generally want to be dynamic, so it will be as tall as the contents inside of it. We’ll address the dynamic height in a moment. For now we’ll address the fixed width problem.

Setup Vertical Scrolling

No we’ll insure that the width of the container view does not exceed the size of the window. For this we’ll make the container view equal width to the main view (the view that holds the scrollview). From the view inspector. CTRL drag from the main view to the container view and select Equal Widths from tho popup menu. Figure 3 shows the two views being connected.

Figure3

Figure 3 CTRL dragging from the main view to the container view

Setup Dynamic Container View Height

The last step, isn’t so much a step as a guide. Basically, in order for this too work all the views you put inside of the container view must have a height. Some views will use their intrinsic content height, which will be determined by the width typically. As a general rule, you’ll need to specify a height and width for any view that does not contain text. Those views that do contain text should at least have a specified width or margins.

The first and last view should be pinned to the top and bottom of the container view respectively. For an example see Figure4.png. Notice in figure 4 that all views have left and right constraints which will determine the width of each view. The labels do not have a height constraint as that will be determined by intrinsic content size, and the square view has an explicit height constraint. Also notice there is an explicit vertical constraint between all views and the top and bottom. Together this will determine the height of the container view and subsequently the content size of the scrollView.

In Figure 4 the following constraints are shown:

  • Top Label

    • Top Constraint to container view: 50pt
    • Left Constraint: 15pt
    • Right Constraint: 15pt
    • Bottom Constraint (same as box top constraint): Standard
  • Box Label

    • Top Constraint(same constraint as top label bottom): Standard
    • Left Constraint: 15pt
    • Right Constraint: 15pt
    • Bottom Constraint (same as bottom label top constraint): Standard
    • Height Constraint: 86pt;
  • Bottom Label

    • Top Constraint (same as box bottom constraint): standard
    • Left Constraint: 15pt
    • Right Constraint: 15pt
    • Bottom Constraint to container view: Standard

Figure4

Figure 4 An example of adding subviews to the container view

Note: If you want your label content to grow then select the label and choose “0” for the number of lines in the property inspector. This will make the number of lines unlimited.

Now if you run this example and put a bunch of text in the bottom label before you do so, will setup the conditions for scrolling vertically. Figure 5 shows the stacked views. You can see in Figure 5 the individual views shown in Figure 4 on top of the container view. The container view is then stacked ontop of the scroll view, which is on the main view, and finally the main window.

Figure 5: The simulated stacked views

Figure5

Setting up Vertical ScrollView Programmatically

Ok, this section will hopefully solidify the concepts from the last section. Start a new single-view application project and open up the ViewController.h file.

Note: If you’re comfortable with XCode you can also drag a new view controller into your existing project’s storyboard, create a class, and then set the class of the storyboard view controller to the new class you created. You can do this from the Identity Inspector on the right. Then you can put the classes inside of a tab bar.

Inside the the ViewController.h header file, you’ll want to create the following properties:

  • UIView named “contentView”
  • UIScrollView named “scrollView”
  • 2 x UILabel named “topLabel” and “bottomLabel”
  • UIView named “boxView”

When you are done your ViewController.h file should look like Code Chunk 1.

Code Chunk 1: Setting up the properties.


1
2
3
4
5
6
7
8
9
10
11
@interface ViewControllerTwo : UIViewController

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

@property (strong, nonatomic) UILabel *topLabel;
@property (strong, nonatomic) UILabel *bottomLabel;

@property (strong, nonatomic) UIView *boxView;

@end

Now in the ViewController.m file we’ll first need to setup the fixed width scroll view in the viewDidLoad method. Start by adding the scrollview and content views as shown in Code Chunk 2.

Code Chunk 2: Adding the scroll view and content view to the main view.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)viewDidLoad{

    [super viewDidLoad];

    self.scrollView = [[UIScrollView alloc] init];
    self.scrollView.translatesAutoresizingMaskIntoConstraints = NO;
    self.scrollView.backgroundColor = [UIColor blueColor];
    [self.view addSubview:self.scrollView];

    self.contentView = [[UIView alloc] init];
    self.contentView.backgroundColor = [UIColor redColor];
    self.contentView.translatesAutoresizingMaskIntoConstraints = NO;
    [self.scrollView addSubview:self.contentView];

    //....

Again we’ll want to pin the scrollview with 0 margins to the main view and then pin the contentView with 0 margins to the contentView. Finally we set the width of the main view to the width of the contentView. This is all done with programmatic constraints. Add this code to the end of the viewDidLoad method as shown in Code Chunk 3

Code Chunk 3: Adding contentView and scrollView constraints.


1
2
3
4
5
6
7
8
9
10
NSDictionary *tmpViewsDictionary = @{@"scrollView":self.scrollView,
                                     @"contentView":self.contentView};

[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(0)-[scrollView]-(0)-|" options:0 metrics:nil views:tmpViewsDictionary]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(0)-[scrollView]-(0)-|" options:0 metrics:nil views:tmpViewsDictionary]];

[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(0)-[contentView]-(0)-|" options:0 metrics:nil views:tmpViewsDictionary]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(0)-[contentView]-(0)-|" options:0 metrics:nil views:tmpViewsDictionary]];

[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.contentView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeWidth multiplier:1 constant:0]];

In this code we are using two different styles of constraints. The first style seen is using the visual format language. Which is a nice way of adding multiple constraints with a string description. In our example for instance:

  • | means superview
    • means space between views (single – means standard) eg. -(0)- vs. –
  • (some value) means width
  • [some view] mean a view

so chaining them together as shown in the first constraint line: @”V:|-(0)-[scrollView]-(0)-|”
This line means, pin the scrollview to the left of the superview with 0 margin and do the same to the right.

For more on the visual format language check out the documentation : Visual Format Language

The width constraint is the standard way of adding constraints to a view. As you can see it’s a bit more explicit but also much more verbose.

Finally, we’ll make a method call to a new method that will add the remaining views to the content view. We’ll call it addContentSubViews. Code Chunk 4 shows the complete viewDidLoad method.

Code Chunk 4: the complete viewDidLoad method.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- (void)viewDidLoad{

    [super viewDidLoad];

    self.scrollView = [[UIScrollView alloc] init];
    self.scrollView.translatesAutoresizingMaskIntoConstraints = NO;
    self.scrollView.backgroundColor = [UIColor blueColor];
    [self.view addSubview:self.scrollView];

    self.contentView = [[UIView alloc] init];
    self.contentView.backgroundColor = [UIColor redColor];
    self.contentView.translatesAutoresizingMaskIntoConstraints = NO;
    [self.scrollView addSubview:self.contentView];


    //Auto Layout Constraints for scrolling content view

    NSDictionary *tmpViewsDictionary = @{@"scrollView":self.scrollView,
                                         @"contentView":self.contentView};

    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(0)-[scrollView]-(0)-|" options:0 metrics:nil views:tmpViewsDictionary]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(0)-[scrollView]-(0)-|" options:0 metrics:nil views:tmpViewsDictionary]];

    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(0)-[contentView]-(0)-|" options:0 metrics:nil views:tmpViewsDictionary]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(0)-[contentView]-(0)-|" options:0 metrics:nil views:tmpViewsDictionary]];

    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.contentView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeWidth multiplier:1 constant:0]];

    [self addContentSubViews];
}

The addContentSubViews method is pretty straightforward, we’ll just create a couple labels, and a box view. The labels have numberOfLines equal to zero which means the content can grow to as many lines as needed. The labels are also centered and word wrapped. Code Chunk 5 shows the full method that you should add underneath the viewDidLoad method.

Code Chunk 5: Implementing the addContentSubViews method


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
- (void)addContentSubViews{

    self.topLabel = [[UILabel alloc] init];
    self.topLabel.translatesAutoresizingMaskIntoConstraints = NO;
    self.topLabel.numberOfLines = 0;
    self.topLabel.textAlignment = NSTextAlignmentCenter;
    self.topLabel.lineBreakMode = NSLineBreakByWordWrapping;
    self.topLabel.text = @"Some text label. that may have several lines";
    self.topLabel.textColor = [UIColor blackColor];
    [self.contentView addSubview:self.topLabel];

    self.boxView = [[UIView alloc] init];
    self.boxView.translatesAutoresizingMaskIntoConstraints = NO;
    self.boxView.backgroundColor = [UIColor lightGrayColor];
    [self.contentView addSubview:self.boxView];

    self.bottomLabel = [[UILabel alloc] init];
    self.bottomLabel.numberOfLines = 0;
    self.bottomLabel.textAlignment = NSTextAlignmentCenter;
    self.bottomLabel.lineBreakMode = NSLineBreakByWordWrapping;
    self.bottomLabel.translatesAutoresizingMaskIntoConstraints = NO;
    self.bottomLabel.text = [self bottomLabelText];
    self.bottomLabel.textColor = [UIColor blackColor];
    [self.contentView addSubview:self.bottomLabel];

    [self addContentSubViewConstraints];

}

In Code Chunk 5 we’re implementing two new methods. One simply returns a large string. Code Chunk 6 show the implementation of the bottomLabelText method.

Code Chunk6: implementing the bottomLabelText method.


1
2
3
4
5
- (NSString *)bottomLabelText{

    return @"Put in a massive string of your own here to see the scrolling in action";

    }

The last method is going to add all the constraints needed to define the content height of the content view. You’ll see from Code Chunk 7 the final implementation of the addContentSubViewConstraints method. Which add all of the exact same constraints as shown in the interface builder version.


- (void)addContentSubViewConstraints{

    NSDictionary *tmpViewsDictionary = @{@"topLabel":self.topLabel,
                                         @"boxView":self.boxView,
                                         @"bottomLabel":self.bottomLabel};

    [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(60)-[topLabel]-[boxView(86)]-[bottomLabel]-|" options:0 metrics:nil views:tmpViewsDictionary]];

    [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(15)-[topLabel]-(15)-|" options:0 metrics:nil views:tmpViewsDictionary]];
    [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(15)-[bottomLabel]-(15)-|" options:0 metrics:nil views:tmpViewsDictionary]];
    [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(15)-[boxView]-(15)-|" options:0 metrics:nil views:tmpViewsDictionary]];

    [self.contentView addConstraint:[NSLayoutConstraint constraintWithItem:self.contentView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.boxView attribute:NSLayoutAttributeCenterX multiplier:1 constant:0]];

}

And that’s pretty much it for the programmatic version. Code Chunk 7 shows the entire viewController.m file.

Code Chunk 7: The full view controller implementation file.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#import "ViewController.h"

@implementation ViewController

- (void)viewDidLoad{

    [super viewDidLoad];

    self.scrollView = [[UIScrollView alloc] init];
    self.scrollView.translatesAutoresizingMaskIntoConstraints = NO;
    self.scrollView.backgroundColor = [UIColor blueColor];
    [self.view addSubview:self.scrollView];

    self.contentView = [[UIView alloc] init];
    self.contentView.backgroundColor = [UIColor redColor];
    self.contentView.translatesAutoresizingMaskIntoConstraints = NO;
    [self.scrollView addSubview:self.contentView];


    //Auto Layout Constraints for scrolling content view

    NSDictionary *tmpViewsDictionary = @{@"scrollView":self.scrollView,
                                         @"contentView":self.contentView};

    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(0)-[scrollView]-(0)-|" options:0 metrics:nil views:tmpViewsDictionary]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(0)-[scrollView]-(0)-|" options:0 metrics:nil views:tmpViewsDictionary]];

    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(0)-[contentView]-(0)-|" options:0 metrics:nil views:tmpViewsDictionary]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(0)-[contentView]-(0)-|" options:0 metrics:nil views:tmpViewsDictionary]];

    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.contentView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeWidth multiplier:1 constant:0]];

    [self addContentSubViews];
}

- (void)addContentSubViews{

    self.topLabel = [[UILabel alloc] init];
    self.topLabel.translatesAutoresizingMaskIntoConstraints = NO;
    self.topLabel.numberOfLines = 0;
    self.topLabel.textAlignment = NSTextAlignmentCenter;
    self.topLabel.lineBreakMode = NSLineBreakByWordWrapping;
    self.topLabel.text = @"Some text label. that may have several lines";
    self.topLabel.textColor = [UIColor blackColor];
    [self.contentView addSubview:self.topLabel];

    self.boxView = [[UIView alloc] init];
    self.boxView.translatesAutoresizingMaskIntoConstraints = NO;
    self.boxView.backgroundColor = [UIColor lightGrayColor];
    [self.contentView addSubview:self.boxView];

    self.bottomLabel = [[UILabel alloc] init];
    self.bottomLabel.numberOfLines = 0;
    self.bottomLabel.textAlignment = NSTextAlignmentCenter;
    self.bottomLabel.lineBreakMode = NSLineBreakByWordWrapping;
    self.bottomLabel.translatesAutoresizingMaskIntoConstraints = NO;
    self.bottomLabel.text = [self bottomLabelText];
    self.bottomLabel.textColor = [UIColor blackColor];
    [self.contentView addSubview:self.bottomLabel];

    [self addContentSubViewConstraints];

}

- (NSString *)bottomLabelText{

    return @"Mauris utinam singularis nostrud et vel et defui aliquip duis. Regula suscipere vel ratis damnum in vindico voco verto antehabeo sit bene. Singularis decet capto luptatum sit delenit suscipit aliquip consequat quis nullus ex.Gemino foras te pala consequat refero abbas in vel. Eum nimis commoveo eros eu. Facilisi in pagus gemino exputo quadrum conventio erat. Haero loquor ut quis sudo immitto adsum sit multo proprius esse.Iustum esse si reprobo utrum et vero ad loquor ne. Duis in nulla. Nutus autem brevitas meus iriure verto ullamcorper velit facilisi. Scisco minim damnum quis transverbero eligo nunc nibh tego.Pala vereor uxor ratis macto enim feugiat iustum os delenit. Antehabeo valetudo vel. Neo patria et iaceo nutus. Ut vero veniam ventosus duis consequat verto. Opto neque nonummy. Duis scisco quidne vero nostrud quidne exputo adsum meus qui. Zelus uxor nobis consequat uxor augue decet. Indoles populus consequat iusto et facilisis pecus nunc feugiat vel valde. Delenit sit nisl indoles minim incassum utinam epulae quae euismod dolor tation. Multo ut vero indoles exputo commoveo. Scisco molior tamen ille. Luptatum cogo accumsan luptatum eu fatua usitas. Molior bene elit paratus sed consequat augue veniam probo patria. Nutus quidem feugiat nonummy ad delenit facilisis ea quibus suscipit. Refero utrum torqueo feugait blandit aliquip ad vulputate cui ideo. Nunc vulputate paulatim dolor volutpat vel brevitas. Reprobo iusto vindico. Qui quis commodo augue nostrud nulla eu consequat minim at imputo. Iriure ullamcorper feugait genitus scisco in scisco obruo jus. Consequat abdo quae dignissim iusto suscipere nulla ad jugis duis virtus. Enim vulputate luptatum in voco haero. Feugiat euismod validus sudo uxor abbas. Ingenium obruo neo. Blandit consequat luptatum euismod sino utrum tego suscipit dignissim suscipit. Sed gilvus utrum in capto Velit ventosus adsum delenit et. Vel verto quidem sit qui vulputate ut autem. Accumsan distineo wisi populus hendrerit ne indoles ille facilisis ut erat hendrerit. Populus sino velit premo dolore neque. Augue ulciscor blandit venio facilisi capto quae praesent ad. Vero opto interdico a roto eros abico. Olim eros ad comis incassum wisi consequat dolus molior oppeto in voco. Genitus caecus duis usitas nisl illum suscipit nulla importunus melior autem. Ulciscor tum quia feugiat paratus olim quod quidem. Duis consequat refoveo nulla refoveo nulla wisi nostrud velit. Neque et caecus ne ad occuro nutus diam vulputate. Populus eros quis ne at quia sit luctus. Adipiscing verto olim et virtus luctus nimis foras nisl in eum mos. Imputo saepius lenis reprobo vero. Aliquam probo ea imputo vicis et suscipere. Vulpes iusto imputo dignissim. Dolore aptent feugiat qui et nibh vicis modo abigo. Sit verto minim feugiat nulla praemitto caecus capto lucidus ullamcorper. Fere eu duis facilisi torqueo.";
}

- (void)addContentSubViewConstraints{

    NSDictionary *tmpViewsDictionary = @{@"topLabel":self.topLabel,
                                         @"boxView":self.boxView,
                                         @"bottomLabel":self.bottomLabel};

    [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(60)-[topLabel]-[boxView(86)]-[bottomLabel]-|" options:0 metrics:nil views:tmpViewsDictionary]];

    [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[topLabel]-|" options:0 metrics:nil views:tmpViewsDictionary]];
    [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[bottomLabel]-|" options:0 metrics:nil views:tmpViewsDictionary]];
    [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[boxView]-|" options:0 metrics:nil views:tmpViewsDictionary]];

    [self.contentView addConstraint:[NSLayoutConstraint constraintWithItem:self.contentView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.boxView attribute:NSLayoutAttributeCenterX multiplier:1 constant:0]];

}

@end

Oh, and let’s not forget swift, Code Chunk 8 shows the full programmatic implementation for Swift.

Code Chunk 8: The Full Swift Controller.



GeSHi Error: GeSHi could not find the language swift (using path /home1/jhoffman/nscookbook/wp-content/plugins/codecolorer/lib/geshi/) (code 2)

Note: For swift I actually had to define a height for the the content view. Which doesn’t make any sense currenty, as it’s almost verbatim the same code translated to swift. Currently it feels like a swift bug, but I’ll keep ya’ll posted. I’m guess it will behave a bit better in swift 2.

Alright, That’s it for this installment. Hope it helps!

About Joe Hoffman

I am An Electrical Engineer that spends a lot of my off time doing web development and other programming. Currently I am trying to expand my knowledge with iOS and I find that writing tutorials helps me learn more thoroughly as well as provide some useful info to others.

Comments

  1. I’ve written custom UI for years so it was massively embarrassing when my new boss saw me scratching my head on a simple scroll view. “I know it has a contentSize, ope, that doesn’t work. Ok, it uses its subviews to determine the height, nope, that doesn’t work. What if I turn off translatesAutoResizeMasks… nope,, on, nope. WHAT THE HECK!”
    Without your random Swift tip I would’ve never had the last key ingredient.
    Ok, I would’ve eventually just spammed “Maybe it needs content size” but you definitely sped up that attempt.

    Now to undo all my commented out AB tests with the resize masks. I have no idea what step of the debugging I’m in with that part. Committing this, to memory and versioning. I’m so happy and so frustrated right now. Awesome blog! I should blog, but too busy writing things… banging head against things it seems also

  2. Hi, i’m new to iOS development. It would be good if there is swift version of this post. Thanks.

    • Joe Hoffman says:

      slowly I’ll get to shifting this stuff, but I can say that translating to swift if pretty easy if you can understand the principles here. autocomplete is your friend!

Speak Your Mind

*

css.php
Privacy Policy