Uiscrollview with iOS Auto Layout Constraints: Wrong Size for Subviews

UIScrollView with iOS Auto Layout Constraints: Wrong size for subviews

A couple of observations:

  1. Constraints for subviews in scroll views don't work like constraints in other views. They're used to set the contentSize of the scroll view. (See TN2154.) That way, you throw a bunch of stuff on a scroll view, set the constraints for the stuff inside it, and the contentSize is calculated for you. It's very cool feature, but it's antithetical to what you're trying to do here.

  2. Worse, buttons will, unless you set an explicit constraint for their width and height of a button, will resize according to their content.

The net effect of these two observations is that your existing constraints say "(a) set my container to be the size of my button; (b) let my button resize itself dynamically to the size of the text; and (c) set my scrollview's contentSize according to the size of my container (which is the size of the button)."

I'm unclear as to what the business problem is. But here are some constraints that achieve what I think your technical question was:

- (void)viewDidLoad
{
[super viewDidLoad];

UIView *view = self.view;

UIScrollView *scrollView = [[UIScrollView alloc] init];
scrollView.backgroundColor = [UIColor redColor]; // just so I can see it
scrollView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:scrollView];

UIView *containerView = [[UIView alloc] init];
containerView.backgroundColor = [UIColor yellowColor]; // just so I can see it
containerView.translatesAutoresizingMaskIntoConstraints = NO;
[scrollView addSubview:containerView];

UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
button.translatesAutoresizingMaskIntoConstraints = NO;
[button setTitle:@"I'm the right size" forState:UIControlStateNormal];
[containerView addSubview:button];

NSDictionary *views = NSDictionaryOfVariableBindings(scrollView, button, view, containerView);

// set the scrollview to be the size of the root view

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

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

// set the container to the size of the main view, and simultaneously
// set the scrollview's contentSize to match the size of the container

[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[containerView(==view)]|"
options:0
metrics:nil
views:views]];

[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[containerView(==view)]|"
options:0
metrics:nil
views:views]];

// set the button size to be the size of the container view

[containerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[button(==containerView)]"
options:0
metrics:nil
views:views]];

[containerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[button(==containerView)]"
options:0
metrics:nil
views:views]];

}

Frankly, I don't understand the business intent of your UI, as this feels like a contortion of auto layout to achieve a very simply UI. I don't know why you have a scroll view if you have "screen sized" content in it (unless you were paging through buttons). I don't know why you'd have a content view with a single item in it. I don't understand why you're using a full-screen button (I'd just put a tap gesture on the root view at that point and call it a day).

I'll assume you have good reasons for all of this, but it might make sense to back up, ask what is your desired user experience is, and then approach the problem fresh to see if there's a more efficient way to achieve the desired effect.

UIScrollView stops scrolling with Autolayout or after zooming

For your approach...

You want to constrain your contentView to the scroll view's Content Layout Guide. This will automatically determine the "scrollable" area.

Since you're not using auto-layout for the contentView's subviews, you'll need to update the .contentView Width and Height constraints each time you add a new subview.

Here's an example. We'll create a MyScrollView at 40-pts from Top / Leading / Trailing with a Height of 240-pts .

The subview will have a Green background, the contentView will have a Blue background, and the scroll view will have a Red background (so we can easily see the frames).

We'll start with just ONE subview to make it easy to see what happens. At first, with only one small subview, there will be no scrolling.

Each time we tap anywhere, we'll add a new Subview to the contentView, with a Yellow background, and update the contentView's Width and Height constraints as needed. You'll see that the Blue content view gets bigger to match the subviews. As soon as the subviews cause the content view to be larger than the width or height of the scroll view, scrolling will be automatic.

public class MyScrollView: UIScrollView {

private var contentView:UIView!

// contentView's Width and Height constraints
// we'll update the .constant values when we add subviews
private var cvWidthConstraint: NSLayoutConstraint!
private var cvHeightConstraint: NSLayoutConstraint!

override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
self.translatesAutoresizingMaskIntoConstraints = false
self.isScrollEnabled = true
self.isDirectionalLockEnabled = true
self.showsHorizontalScrollIndicator = true
self.showsVerticalScrollIndicator = false
self.decelerationRate = .normal
self.delaysContentTouches = false
self.bouncesZoom = true

setupSubviews()

}

private func setupSubviews() {

contentView = UIView()
contentView.backgroundColor = UIColor.clear
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.isUserInteractionEnabled = true

self.addSubview(contentView)

// constrain contentView to scroll view's Content Layout Guide
// this determines the "scrollable" area
contentView.leadingAnchor.constraint(equalTo: self.contentLayoutGuide.leadingAnchor).isActive = true
contentView.trailingAnchor.constraint(equalTo: self.contentLayoutGuide.trailingAnchor).isActive = true
contentView.topAnchor.constraint(equalTo: self.contentLayoutGuide.topAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: self.contentLayoutGuide.bottomAnchor).isActive = true

// create contentView's Width and Height constraints
cvWidthConstraint = contentView.widthAnchor.constraint(equalToConstant: 0.0)
cvHeightConstraint = contentView.heightAnchor.constraint(equalToConstant: 0.0)

// activate them
cvWidthConstraint.isActive = true
cvHeightConstraint.isActive = true

//Add other subviews to contentView

// we'll start with ONE subview, so we can easily see what's happening
let subviewWidth = CGFloat(240)
let subviewHeight = CGFloat(20)

let subview = UILabel(frame: CGRect(x: 5, y: 5, width: subviewWidth, height: subviewHeight))
subview.textAlignment = .center
subview.text = "First"
subview.backgroundColor = .green
contentView.addSubview(subview)

// so we can see the frames
self.backgroundColor = .red
self.contentView.backgroundColor = .blue

// update the contentView constraints
updateContent()
}

private func updateContent() -> Void {
// array of subviews
let views = contentView.subviews
// get the
// max Y of the subview frames
// max X of the subview frames
guard let maxYValue = views.lazy.map({ $0.frame.maxY }).max(),
let maxXValue = views.lazy.map({ $0.frame.maxX }).max()
else { return }

// update contentView Width and Height constraints
cvWidthConstraint.constant = maxXValue + 5.0
cvHeightConstraint.constant = maxYValue + 5.0
}

func addLabel(frame _frame: CGRect, text: String) -> Void {

// add a new subview
let subview = UILabel(frame: _frame)
subview.textAlignment = .center
subview.text = text
subview.backgroundColor = .yellow
contentView.addSubview(subview)

// update the contentView constraints
updateContent()

}
}

class ExampleViewController: UIViewController {

let myScrollView = MyScrollView()

var count: Int = 1

override func viewDidLoad() {
super.viewDidLoad()

view.addSubview(myScrollView)

let g = view.safeAreaLayoutGuide

NSLayoutConstraint.activate([

// constrain custom scroll view Top / Leading / Trailing
// 40-pts from the safe-area edges
myScrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
myScrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
myScrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),

// scroll view Height: 240-pts
myScrollView.heightAnchor.constraint(equalToConstant: 240.0),
])

// add tap gesture recognizer so we can add a new subview
// every time we tap
let t = UITapGestureRecognizer(target: self, action: #selector(self.testAddSubview))
view.addGestureRecognizer(t)
}

@objc func testAddSubview() -> Void {
let s = "New Subview \(count)"
let x: CGFloat = CGFloat(count) * 60.0
let y: CGFloat = CGFloat(count) * 35.0
myScrollView.addLabel(frame: CGRect(x: x, y: y, width: 200, height: 30), text: s)
count += 1
}
}

On launch - one subview - no scrolling:

Sample Image

After adding one new subview - blue content view is larger, but not big enough for scrolling:

Sample Image

After adding 4 new subviews - now we have scrolling:

Sample Image

Full width subviews of uiscrollview with auto layout in iOS

After some more years of iOS development and auto layout, I found that fixing this is quite easy. Just set and equal width constraint between the collection view and the root view, ie the superview of the scrollview.

Strange behaviour adding constraints to subview of UIScrollView within a UIStoryboard

Scrollviews are a bit tricky, because they're made to scroll over their content both horizontally and vertically.

If you think about it, there's no way for the scrollview to know its content width, you have to set it. That's why your button is not where it should be.

If you want your view to scroll only vertically, you first have to set your content width to your scrollview's width (more exactly to your scrollview's parent's width).

The usual way to achieve this is to have a unique subview into your scrollView, and use it as a content view.
So, add a UIView to your scrollview, add it the constraints so that it fills the scrollview (ie top, leading, trailing and bottom spacing = 0).
Next, the most important constraint: add a width constraint so that your content view's width is equal to the scrollview's parent's width (in your case, probably your viewController's view.

And that's it. Now you can add any content you want into the content view. (your content view needs to have an height too, either a fixed height or constraints from the top to bottom)

ScrollView Setup

Autolayout: Incorrect Width when using ScrollView

The UIImageView.image.size.width is 1000 in your case. So it's following it's own intrinsic content size and expanding itself with respect to own intrinsic content size. You can follow one of the method so solve this:

Method 1:- Add width constraint of contentView, so UIImageView will adopt the width of contentView.

Method 2:- Add width constraint of UIImageView, so contentView will adopt the width of UIImageView.

Method 3:- Add Equal Width constraint on your contentView and UIScrollView. so contentView will adopt the scrollView width, and UIImageView will adopt contentView width.

You can get more details here about the contents intrinsic content size:
http://www.raywenderlich.com/20881/beginning-auto-layout-part-1-of-2

Getting ScrollView to Work with Autolayout and Storyboard

You have to provide the contentSize for a UIScrollView when laying it out using constraints.

For example:

You can set the width of View to be equal to the scrollView as this will provide the width of the scrollview's content. Then, to provide content height, layout red and yellow views vertically and use them to provide the height of View. This means you'll have to provide initial height for yellow view, or you can use a view that provides an intrinsicContentSize to determine the yellow view height so you don't have to manually change it.

Also I think you have to remove redview top == layout guide top, and add redview top == View top and yellowview bottom == View bottom

Calculating contentSize for UIScrollView when using Auto Layout

Giving content size programatically is not good way. The below solution which will
work using autolayout, dont need to set content size at all. It will calculate as per
how many UI fields added to view.

Step 1 :

Add Scrollview to view in storyboard and add leading, trailing, top and bottom constraints
(All values are zero).

Step 2 :

Don't add directly views which you need on directly scrollview, First add one view
to scrollview (that will be our content view for all UI elements).
Add below constraints to this view.

1) Leading, trailing, top and bottom constraints
(All values are zero).

2) Add equal height, equal width to Main view (i.e. which contains scrollview).
For equal height set priority to low. (This is the important step for setting content size).

3) Height of this content view will be according to the number of views added to the view.
let say if you added last view is one label and his Y position is 420 and height
is 20 then your content view will be 440.

Step 3 : Add constraints to all of views which you added within content view as per your requirement.

For reference :

Sample Image

Sample Image

I hope this will definitely help you.



Related Topics



Leave a reply



Submit