How to automatically size UIScrollView to fit the content
Asked Answered
G

25

140

Is there a way to make a UIScrollView auto-adjust to the height (or width) of the content it's scrolling?

Something like:

[scrollView setContentSize:(CGSizeMake(320, content.height))];
Garnishee answered 31/5, 2010 at 14:54 Comment(0)
H
316

The best method I've ever come across to update the content size of a UIScrollView based on its contained subviews:

Objective-C

CGRect contentRect = CGRectZero;

for (UIView *view in self.scrollView.subviews) {
    contentRect = CGRectUnion(contentRect, view.frame);
}
self.scrollView.contentSize = contentRect.size;

Swift

let contentRect: CGRect = scrollView.subviews.reduce(into: .zero) { rect, view in
    rect = rect.union(view.frame)
}
scrollView.contentSize = contentRect.size
Hanzelin answered 24/6, 2013 at 19:37 Comment(6)
I was running this in viewDidLayoutSubviews so the autolayout would finish, in iOS7 it worked well, but testing os iOS6 the autolayout for some reason didn't finish the work, so I had some wrong height values, so I switched to viewDidAppear now works fine.. just to point out maybe someone would need this. thanksInstantaneity
With iOS6 iPad there was mysterious UIImageView (7x7) at bottom right corner. I filtered that away by accepting only (visible && alpha) UI components, then it worked. Wondering if that imageView was related to scrollBars, definitely not my code.Identification
Please be careful when Scroll Indicators are enabled! a UIImageView will be automatically created for each one by the SDK. you must account for it or else your content size will be wrong. ( see #5389203 )Sunless
Can confirm that this solution works perfectly for me in Swift 4Megalopolis
Actually, we cannot start unioning from CGRect.zero, it works only if you home some subviews in negative coordinates. You should start union subview frames from the first subview, not from zero. For instance, you have just one subview which is an UIView with frame (50, 50, 100, 100). Your content size will be (0, 0, 150, 150), not (50, 50, 100, 100).Splenius
Is it possible to calculate size if child subviews inserted via AutoLayout with offset? This top variant counts only frames without counting distance between.Jolt
B
73

UIScrollView doesn't know the height of its content automatically. You must calculate the height and width for yourself

Do it with something like

CGFloat scrollViewHeight = 0.0f;
for (UIView* view in scrollView.subviews)
{
   scrollViewHeight += view.frame.size.height;
}

[scrollView setContentSize:(CGSizeMake(320, scrollViewHeight))];

But this only work if the views are one below the other. If you have a view next to each other you only have to add the height of one if you don't want to set the content of the scroller larger than it really is.

Bess answered 31/5, 2010 at 15:24 Comment(4)
Do you think something like this would work as well? CGFloat scrollViewHeight = scrollView.contentSize.height; [scrollView setContentSize:(CGSizeMake(320, scrollViewHeight))]; BTW, don't forget to ask for the size then height. scrollViewHeight += view.frame.size.heightGarnishee
Initializing the scrollViewHeight to the height makes the scroller larger than its content. If you do this don't add the height of the views visible on the initial height of the scroller content. Although maybe you need an initial gap or something else.Bess
Or just do: int y = CGRectGetMaxY(((UIView*)[_scrollView.subviews lastObject]).frame); [_scrollView setContentSize:(CGSizeMake(CGRectGetWidth(_scrollView.frame), y))];Emanuele
@saintjab, thanks, I just added it as a formal answer :)Emanuele
N
48

Solution if you're using auto layout:

  • Set translatesAutoresizingMaskIntoConstraints to NO on all views involved.

  • Position and size your scroll view with constraints external to the scroll view.

  • Use constraints to lay out the subviews within the scroll view, being sure that the constraints tie to all four edges of the scroll view and do not rely on the scroll view to get their size.

Source: Technical Note TN2154 UIScrollView And Autolayout.

Nominative answered 27/3, 2014 at 22:17 Comment(5)
Imho this is the real solution in the world where everything happens with auto layout. Regarding the other solutions: What the... are you serious in calculating every view's heigh and sometimes width?Reinwald
Thank you for sharing this! That should be the accepted answer.Broadcasting
This doesn't work when you want to height of the scrollview to be based on its content height. (for instance a popup centered in the screen where you don't want to scroll until a certain amount of content).Straka
This does not answer the questionPeng
Great solution. However, I'd add one more bullet..."Don't restrict the height of a child stack view if it should grow vertically. Just pin it to the edges of the scroll view."Goldarn
M
35

I added this to Espuz and JCC's answer. It uses the y position of the subviews and doesn't include the scroll bars. Edit Uses the bottom of the lowest sub view that is visible.

+ (CGFloat) bottomOfLowestContent:(UIView*) view
{
    CGFloat lowestPoint = 0.0;

    BOOL restoreHorizontal = NO;
    BOOL restoreVertical = NO;

    if ([view respondsToSelector:@selector(setShowsHorizontalScrollIndicator:)] && [view respondsToSelector:@selector(setShowsVerticalScrollIndicator:)])
    {
        if ([(UIScrollView*)view showsHorizontalScrollIndicator])
        {
            restoreHorizontal = YES;
            [(UIScrollView*)view setShowsHorizontalScrollIndicator:NO];
        }
        if ([(UIScrollView*)view showsVerticalScrollIndicator])
        {
            restoreVertical = YES;
            [(UIScrollView*)view setShowsVerticalScrollIndicator:NO];
        }
    }
    for (UIView *subView in view.subviews)
    {
        if (!subView.hidden)
        {
            CGFloat maxY = CGRectGetMaxY(subView.frame);
            if (maxY > lowestPoint)
            {
                lowestPoint = maxY;
            }
        }
    }
    if ([view respondsToSelector:@selector(setShowsHorizontalScrollIndicator:)] && [view respondsToSelector:@selector(setShowsVerticalScrollIndicator:)])
    {
        if (restoreHorizontal)
        {
            [(UIScrollView*)view setShowsHorizontalScrollIndicator:YES];
        }
        if (restoreVertical)
        {
            [(UIScrollView*)view setShowsVerticalScrollIndicator:YES];
        }
    }

    return lowestPoint;
}
Miserere answered 14/6, 2011 at 1:2 Comment(7)
Wonderful! Just what I needed.Crosscountry
I'm surprised a method like this one not part of the class.Guitarfish
Why did you do self.scrollView.showsHorizontalScrollIndicator = NO; self.scrollView.showsVerticalScrollIndicator = NO; in the beginning and self.scrollView.showsHorizontalScrollIndicator = YES; self.scrollView.showsVerticalScrollIndicator = YES; in the end of the method?Guitarfish
self is the owner of the scroll viewMiserere
Added showing and hiding of scroll indicator because scrollView.subviews can count the indicators as subviews. Not sure what causes it to count them sometimes.Miserere
@Miserere you're right scroll indicators are subviews if visible, but showing/hiding logic is wrong. You need to add two BOOL locals: restoreHorizontal=NO,restoreVertical=NO. If showsHorizontal, hide it, set restoreHorizontal to YES. Same for vertical. Next, after for/in loop insert if statement: if restoreHorizontal, set to show it, same for vertical. Your code will just force to show both indicatorsPerfective
good point taras.roshko! updating code also added checks for responding to set show scroll indicators so it can be used with a normal UIViewMiserere
M
14

Here is the accepted answer in swift for anyone who is too lazy to convert it :)

var contentRect = CGRectZero
for view in self.scrollView.subviews {
    contentRect = CGRectUnion(contentRect, view.frame)
}
self.scrollView.contentSize = contentRect.size

OR lazier yet

let contentRect = self.scrollView.subviews.reduce(CGRect.zero) { CGRect.union($0, $1.frame) }
self.scrollView.contentSize = contentRect.size
Modiste answered 16/7, 2016 at 7:33 Comment(2)
and updated for Swift3: var contentRect = CGRect.zero for view in self.scrollView.subviews { contentRect = contentRect.union(view.frame) } self.scrollView.contentSize = contentRect.sizeParliament
Does this has to be called on main thread? and in didLayoutSubViews?Pucida
L
9

Following extension would be helpful in Swift.

extension UIScrollView{
    func setContentViewSize(offset:CGFloat = 0.0) {
        // dont show scroll indicators
        showsHorizontalScrollIndicator = false
        showsVerticalScrollIndicator = false

        var maxHeight : CGFloat = 0
        for view in subviews {
            if view.isHidden {
                continue
            }
            let newHeight = view.frame.origin.y + view.frame.height
            if newHeight > maxHeight {
                maxHeight = newHeight
            }
        }
        // set content size
        contentSize = CGSize(width: contentSize.width, height: maxHeight + offset)
        // show scroll indicators
        showsHorizontalScrollIndicator = true
        showsVerticalScrollIndicator = true
    }
}

Logic is the same with the given answers. However, It omits hidden views within UIScrollView and calculation is performed after scroll indicators set hidden.

Also, there is an optional function parameter and you're able to add an offset value by passing parameter to function.

Lynnelle answered 14/4, 2016 at 12:39 Comment(1)
This solution contains too many assumptions, why are you showing the scrolling indicators in a method that sets the content size?Bega
R
9

Here's a Swift 3 adaptation of @leviatan's answer :

EXTENSION

import UIKit


extension UIScrollView {

    func resizeScrollViewContentSize() {

        var contentRect = CGRect.zero

        for view in self.subviews {

            contentRect = contentRect.union(view.frame)

        }

        self.contentSize = contentRect.size

    }

}

USAGE

scrollView.resizeScrollViewContentSize()

Very easy to use !

Romeu answered 15/5, 2017 at 17:6 Comment(2)
Very useful. After so many iteration i found this solution and it is perfect.Ot
Lovely! Just implemented it.Mulholland
T
5

Great & best solution from @leviathan. Just translating to swift using FP (functional programming) approach.

self.scrollView.contentSize = self.scrollView.subviews.reduce(CGRect(), { 
  CGRectUnion($0, $1.frame) 
}.size
Told answered 27/11, 2016 at 14:11 Comment(2)
If you want to ignore hidden views you can filter subviews before passing to reduce.Conchita
Updated for Swift 3: self.contentSize = self.subviews.reduce(CGRect(), { $0.union($1.frame) }).sizeVamoose
S
4

You can get height of the content inside UIScrollView by calculate which child "reaches furthers". To calculate this you have to take in consideration origin Y (start) and item height.

float maxHeight = 0;
for (UIView *child in scrollView.subviews) {
    float childHeight = child.frame.origin.y + child.frame.size.height;
    //if child spans more than current maxHeight then make it a new maxHeight
    if (childHeight > maxHeight)
        maxHeight = childHeight;
}
//set content size
[scrollView setContentSize:(CGSizeMake(320, maxHeight))];

By doing things this way items (subviews) don't have to be stacked directly one under another.

Screen answered 21/5, 2013 at 18:43 Comment(1)
You can also use this one liner inside the loop: maxHeight = MAX(maxHeight, child.frame.origin.y + child.frame.size.height) ;)Rationalize
B
3

I came up with another solution based on @emenegro's solution

NSInteger maxY = 0;
for (UIView* subview in scrollView.subviews)
{
    if (CGRectGetMaxY(subview.frame) > maxY)
    {
        maxY = CGRectGetMaxY(subview.frame);
    }
}
maxY += 10;
[scrollView setContentSize:CGSizeMake(scrollView.frame.size.width, maxY)];

Basically, we figure out which element is furthest down in the view and adds a 10px padding to the bottom

Brattishing answered 5/4, 2014 at 16:12 Comment(0)
E
3

Or just do:

int y = CGRectGetMaxY(((UIView*)[_scrollView.subviews lastObject]).frame); [_scrollView setContentSize:(CGSizeMake(CGRectGetWidth(_scrollView.frame), y))];

(This solution was added by me as a comment in this page. After getting 19 up-votes for this comment, I've decided to add this solution as a formal answer for the benefit of the community!)

Emanuele answered 15/12, 2015 at 8:21 Comment(0)
R
3

Because a scrollView can have other scrollViews or different inDepth subViews tree, run in depth recursively is preferable. enter image description here

Swift 2

extension UIScrollView {
    //it will block the mainThread
    func recalculateVerticalContentSize_synchronous () {
        let unionCalculatedTotalRect = recursiveUnionInDepthFor(self)
        self.contentSize = CGRectMake(0, 0, self.frame.width, unionCalculatedTotalRect.height).size;
    }

    private func recursiveUnionInDepthFor (view: UIView) -> CGRect {
        var totalRect = CGRectZero
        //calculate recursevly for every subView
        for subView in view.subviews {
            totalRect =  CGRectUnion(totalRect, recursiveUnionInDepthFor(subView))
        }
        //return the totalCalculated for all in depth subViews.
        return CGRectUnion(totalRect, view.frame)
    }
}

Usage

scrollView.recalculateVerticalContentSize_synchronous()
Rescissory answered 22/3, 2017 at 17:2 Comment(0)
A
2

For swift4 using reduce:

self.scrollView.contentSize = self.scrollView.subviews.reduce(CGRect.zero, {
   return $0.union($1.frame)
}).size
Affiance answered 22/5, 2018 at 12:26 Comment(2)
Also worth checking if at least one subview has origin == CGPoint.zero or just assign a particular (largest for example) subview's origin to zero.Intolerant
This only works for people who place their views with CGRect(). If you are using constraints this is not working.Gambol
Y
1

The size depends on the content loaded inside of it, and the clipping options. If its a textview, then it also depends on the wrapping, how many lines of text, the font size, and so on and on. Nearly impossible for you to compute yourself. The good news is, it is computed after the view is loaded and in the viewWillAppear. Before that, it's all unknown and and content size will be the same as frame size. But, in the viewWillAppear method and after (such as the viewDidAppear) the content size will be the actual.

Yser answered 27/5, 2011 at 3:25 Comment(0)
S
1

Wrapping Richy's code I created a custom UIScrollView class that automates content resizing completely!

SBScrollView.h

@interface SBScrollView : UIScrollView
@end

SBScrollView.m:

@implementation SBScrollView
- (void) layoutSubviews
{
    CGFloat scrollViewHeight = 0.0f;
    self.showsHorizontalScrollIndicator = NO;
    self.showsVerticalScrollIndicator = NO;
    for (UIView* view in self.subviews)
    {
        if (!view.hidden)
        {
            CGFloat y = view.frame.origin.y;
            CGFloat h = view.frame.size.height;
            if (y + h > scrollViewHeight)
            {
                scrollViewHeight = h + y;
            }
        }
    }
    self.showsHorizontalScrollIndicator = YES;
    self.showsVerticalScrollIndicator = YES;
    [self setContentSize:(CGSizeMake(self.frame.size.width, scrollViewHeight))];
}
@end

How to use:
Simply import the .h file to your view controller and declare a SBScrollView instance instead of the normal UIScrollView one.

Sunless answered 19/2, 2013 at 20:15 Comment(2)
layoutSubviews looks like it is the right place for such layout related code. But in a UIScrollView layoutSubview gets called every time the content offset is updated while scrolling. And since the content size usually doesn’t change while scrolling this is rather wasteful.Fante
That's true. One way to avoid this inefficiency might be to check [self isDragging] and [self isDecelerating] and only perform layout if both are false.Sajovich
D
1

why not single line of code??

_yourScrollView.contentSize = CGSizeMake(0, _lastView.frame.origin.y + _lastView.frame.size.height);
Disappoint answered 29/3, 2016 at 8:12 Comment(0)
A
1

I created a subclass of ScrollView to handle the intrinsicContentSize and it worked perfectly for me

public final class ContentSizedScrollView: UIScrollView {
    override public var contentSize: CGSize {
        didSet {
            invalidateIntrinsicContentSize()
        }
    }

    override public var intrinsicContentSize: CGSize {
        layoutIfNeeded()
        return self.contentSize
    }
}

Now you can create a scrollview with this class and set constraints on all sides

Make sure that the subviews are tied up to all fours edges of the scrollView

Accord answered 31/5, 2022 at 15:8 Comment(1)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Ephedrine
M
0

it depends on the content really : content.frame.height might give you what you want ? Depends if content is a single thing, or a collection of things.

Midwinter answered 31/5, 2010 at 15:12 Comment(0)
C
0

I also found leviathan's answer to work the best. However, it was calculating a strange height. When looping through the subviews, if the scrollview is set to show scroll indicators, those will be in the array of subviews. In this case, the solution is to temporarily disable the scroll indicators before looping, then re-establish their previous visibility setting.

-(void)adjustContentSizeToFit is a public method on a custom subclass of UIScrollView.

-(void)awakeFromNib {    
    dispatch_async(dispatch_get_main_queue(), ^{
        [self adjustContentSizeToFit];
    });
}

-(void)adjustContentSizeToFit {

    BOOL showsVerticalScrollIndicator = self.showsVerticalScrollIndicator;
    BOOL showsHorizontalScrollIndicator = self.showsHorizontalScrollIndicator;

    self.showsVerticalScrollIndicator = NO;
    self.showsHorizontalScrollIndicator = NO;

    CGRect contentRect = CGRectZero;
    for (UIView *view in self.subviews) {
        contentRect = CGRectUnion(contentRect, view.frame);
    }
    self.contentSize = contentRect.size;

    self.showsVerticalScrollIndicator = showsVerticalScrollIndicator;
    self.showsHorizontalScrollIndicator = showsHorizontalScrollIndicator;
}
Clint answered 4/6, 2014 at 18:11 Comment(0)
A
0

I think this can be a neat way of updating UIScrollView's content view size.

extension UIScrollView {
    func updateContentViewSize() {
        var newHeight: CGFloat = 0
        for view in subviews {
            let ref = view.frame.origin.y + view.frame.height
            if ref > newHeight {
                newHeight = ref
            }
        }
        let oldSize = contentSize
        let newSize = CGSize(width: oldSize.width, height: newHeight + 20)
        contentSize = newSize
    }
}
Allround answered 12/9, 2015 at 11:50 Comment(0)
C
0

Set dynamic content size like this.

 self.scroll_view.contentSize = CGSizeMake(screen_width,CGRectGetMaxY(self.controlname.frame)+20);
Coastward answered 16/7, 2016 at 8:3 Comment(0)
R
0
import UIKit

class DynamicSizeScrollView: UIScrollView {

    var maxHeight: CGFloat = UIScreen.main.bounds.size.height
    var maxWidth: CGFloat = UIScreen.main.bounds.size.width

    override func layoutSubviews() {
        super.layoutSubviews()
        if !__CGSizeEqualToSize(bounds.size,self.intrinsicContentSize){
            self.invalidateIntrinsicContentSize()
        }
    }

    override var intrinsicContentSize: CGSize {
        let height = min(contentSize.height, maxHeight)
        let width = min(contentSize.height, maxWidth)
        return CGSize(width: width, height: height)
    }

}
Ritaritardando answered 24/4, 2020 at 4:25 Comment(0)
S
0

If you using Auto layout, just set the border element's edge equal to your scroll view.

For example, I wanna my horizontal scroll view auto fit my horizontal contents:

Swift

let bottomConstrint = NSLayoutConstraint.init(item: (bottommost UI element),
                                                  attribute: .bottom,
                                                  relatedBy: .equal,
                                                  toItem: (your UIScrollView),
                                                  attribute: .bottom,
                                                  multiplier: 1.0,
                                                  constant: 0)
bottomConstrint.isActive = true

If you using Snapkit like me, just:

  scrollView.addSubview( (bottommost element) )
  (bottommost element).snp.makeConstraints { make in
      /*other constraints*/
      make.bottom.equalToSuperview()
  }
Schalles answered 10/6, 2022 at 8:48 Comment(0)
S
0
class ContentSizedScrollView: UIScrollView {

    override var contentSize:CGSize {
        didSet {
            invalidateIntrinsicContentSize()
        }
    }

    override var intrinsicContentSize: CGSize {
        layoutIfNeeded()
        return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
    }
}

// In UIViewController

import SnapKit

...

var scrollView: ContentSizedScrollView!

override func viewDidLayoutSubviews() {
  super.viewDidLayoutSubviews()
  scrollView.contentSize = .init(width: view.bounds.width, height: stackView.bounds.height)
}


// Here some example of content composing inside of UIStackView
func setupContent() {
  scrollView = ContentSizedScrollView()
  blockView.addSubview(scrollView)
  scrollView.snp.makeConstraints { make in
      make.top.equalTo(19)
      make.left.equalToSuperview()
      make.right.equalToSuperview()
      make.bottom.equalTo(-20)
  }
  scrollView.contentInset = .init(top: 0, left: 0, bottom: 26, right: 0)
  scrollView.showsVerticalScrollIndicator = false
  scrollView.clipsToBounds = true
  scrollView.layer.cornerRadius = blockView.layer.cornerRadius / 2

  stackView = UIStackView()
  scrollView.addSubview(stackView)
  stackView.snp.makeConstraints { make in
      make.top.equalToSuperview()
      make.centerX.equalToSuperview()
      make.width.equalToSuperview().offset(-10)
  }
  stackView.axis = .vertical
  stackView.alignment = .center

  textTitleLabel = Label()
  stackView.addArrangedSubview(textTitleLabel)
  textTitleLabel.snp.makeConstraints { make in
      make.width.equalToSuperview().offset(-30)
  }
  textTitleLabel.font = UIFont.systemFont(ofSize: 20, weight: .bold)
  textTitleLabel.textColor = Color.Blue.oxfordBlue
  textTitleLabel.textAlignment = .center
  textTitleLabel.numberOfLines = 0
  stackView.setCustomSpacing(10, after: textTitleLabel)
}
Sulphurize answered 16/11, 2022 at 19:20 Comment(0)
A
-1

I would create a subclass of UIScrollView with the following:

class ContentSizedScrollView: UIScrollView {
    override var contentSize:CGSize {
        didSet {
            invalidateIntrinsicContentSize()
        }
    }

    override var intrinsicContentSize: CGSize {
        layoutIfNeeded()
        return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
    }
}

This will resize automatically based on the height of the content.

Anaesthesiology answered 11/10, 2022 at 21:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.