Programmatically Creating Controller with Uiscrollview and Autolayout Is Not Sizing the Views Properly

Programmatically creating controller with UIScrollView and AutoLayout is not sizing the views properly

After much more pain and reading closely, the reason for this behavior and the answer becomes clearer. It is also very well described at this SO question. I wish the Technical Note was more English, but @rdelmar and @Rob explain very clearly that "constraints on contentView are used to set the contentSize of the scrollView".

Here is the modified code and the result (which I think is the correct solution). My view hierarchy is now such:
ViewController -> UIView (mainView) -> UIScrollVIew -> UIView (contentView) -> UIViews (subviews)

There are extra width constraints between the contentView and mainView. In addition, all subviews are added to the contentView rather than being added to scrollView directly.

class ScrollableRowHeadersViewController : UIViewController {

var scrollView : UIScrollView!
var contentView : UIView!

override func loadView() {
super.loadView()
self.view = UIView(frame: CGRectZero)
scrollView = UIScrollView(frame:CGRectZero)
scrollView.sizeToFit()
scrollView.setTranslatesAutoresizingMaskIntoConstraints(false)
self.view.addSubview(scrollView)
scrollView.backgroundColor = UIColor.blueColor()

contentView = UIView()
contentView.setTranslatesAutoresizingMaskIntoConstraints(false)
contentView.backgroundColor = UIColor.redColor()
scrollView.addSubview(contentView)

self.view.addVisualConstraint("H:|-0-[scrollView]-0-|", viewsDict: ["scrollView" : scrollView])
self.view.addVisualConstraint("V:|-0-[scrollView]-0-|", viewsDict: ["scrollView" : scrollView])

self.view.addVisualConstraint("H:|[contentView]|", viewsDict: ["contentView" : contentView])
self.view.addVisualConstraint("V:|[contentView]|", viewsDict: ["contentView" : contentView])

//make the width of content view to be the same as that of the containing view.
self.view.addVisualConstraint("H:[contentView(==mainView)]", viewsDict: ["contentView" : contentView, "mainView" : self.view])

self.view.contentMode = UIViewContentMode.Redraw
}

//load all the subviews after the main view and scrollview loaded.
override func viewDidLoad() {
var viewsDict = [String: UIView]()
viewsDict["contentView"] = contentView
viewsDict["super"] = self.view
var vertical_constraints = "V:|"

for i in 1...50 {
var subview = UIView()
subview.setTranslatesAutoresizingMaskIntoConstraints(false)
subview.backgroundColor = (i%2 == 0 ? UIColor.brownColor() : UIColor.greenColor())
viewsDict["subview_\(i)"] = subview
contentView.addSubview(subview)

vertical_constraints += "[subview_\(i)(==50)]"
self.contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[subview_\(i)]|", options: NSLayoutFormatOptions(0), metrics: nil, views: viewsDict))
}

vertical_constraints += "|"
self.contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat(vertical_constraints, options: NSLayoutFormatOptions.AlignAllLeft, metrics: nil, views: viewsDict))
}

}

Here is the output as I was expecting it to be:

Sample Image

UIScrollView Not Sizing Properly In Autolayout

Solved! Thanks to Working with Top Layout Guide in UIScrollView through Auto Layout. I didn't think a UIScrollView would make topLayoutGuide behave differently, but apparently it does. Also turns out I didn't need to set the constraints; scrollView.layoutIfNeeded() does the trick! My scrollView is now a constant 375x667, removing the resizing issue and giving self.topLayoutGuide.length = 64. This allows me to center the view vertically by subtracting self.topLayoutGuide.length in centerScrollViewContents(). I also call centerScrollViewContents() in viewWillLayoutSubviews(). Here's my new code:

@IBOutlet var scrollView: UIScrollView!

var imageView: UIImageView!

override func viewDidLoad() {
super.viewDidLoad()

//Redo constraints at runtime
scrollView.layoutIfNeeded()

//Original Ray Wenderlich code
let image1 = UIImage(named: "photo1")!
imageView = UIImageView(image: image1)
imageView.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: image1.size)
scrollView.addSubview(imageView)
scrollView.contentSize = image1.size

let scrollViewFrame = scrollView.frame
let scaleWidth = scrollViewFrame.width / scrollView.contentSize.width
let scaleHeight = scrollViewFrame.height / scrollView.contentSize.height
let minScale = min(scaleWidth, scaleHeight)
scrollView.minimumZoomScale = minScale
scrollView.maximumZoomScale = 1.0
scrollView.zoomScale = minScale
}
func centerScrollViewContents() {
let boundsSize = scrollView.bounds.size
var contentsFrame = imageView.frame

if contentsFrame.size.width < boundsSize.width {
contentsFrame.origin.x = (boundsSize.width - contentsFrame.width) / 2.0
} else {
contentsFrame.origin.x = 0.0
}

if contentsFrame.size.height < boundsSize.height {
contentsFrame.origin.y = (boundsSize.height - contentsFrame.height - self.topLayoutGuide.length) / 2.0
} else {
contentsFrame.origin.y = 0.0
}

imageView.frame = contentsFrame
}

override func viewWillLayoutSubviews() {
println(self.topLayoutGuide.length)
centerScrollViewContents()
}

All other functions have not been changed.

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.

iOS Autolayout with UIScrollview: Why does content view of scroll view not fill the scroll view?

Constraints with scroll views work slightly differently than it does with other views. The constraints between of contentView and its superview (the scrollView) are to the scrollView's contentSize, not to its frame. This might seem confusing, but it is actually quite useful, meaning that you never have to adjust the contentSize, but rather the contentSize will automatically adjust to fit your content. This behavior is described in Technical Note TN2154.

If you want to define the contentView size to the screen or something like that, you'd have to add a constraint between the contentView and the main view, for example. That's, admittedly, antithetical to putting content into the scrollview, so I probably wouldn't advise that, but it can be done.


To illustrate this concept, that the size of contentView will be driven by its content, not by the bounds of the scrollView, add a label to your contentView:

UIScrollView* scrollView = [UIScrollView new];
scrollView.translatesAutoresizingMaskIntoConstraints = NO;
scrollView.backgroundColor = [UIColor redColor];
[self.view addSubview:scrollView];

UIView* contentView = [UIView new];
contentView.translatesAutoresizingMaskIntoConstraints = NO;
contentView.backgroundColor = [UIColor greenColor];
[scrollView addSubview:contentView];

UILabel *randomLabel = [[UILabel alloc] init];
randomLabel.text = @"this is a test";
randomLabel.translatesAutoresizingMaskIntoConstraints = NO;
randomLabel.backgroundColor = [UIColor clearColor];
[contentView addSubview:randomLabel];

NSDictionary* viewDict = NSDictionaryOfVariableBindings(scrollView, contentView, randomLabel);

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

[scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[contentView]|" options:0 metrics:0 views:viewDict]];
[scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[contentView]|" options:0 metrics:0 views:viewDict]];

[contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[randomLabel]-|" options:0 metrics:0 views:viewDict]];
[contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[randomLabel]-|" options:0 metrics:0 views:viewDict]];

Now you'll see that the contentView (and, therefore, the contentSize of the scrollView) are adjusted to fit the label with standard margins. And because I didn't specify the width/height of the label, that will adjust based upon the text you put into that label.


If you want the contentView to also adjust to the width of the main view, you could do redefine your viewDict like so, and then add these additional constraints (in addition to all the others, above):

UIView *mainView = self.view;

NSDictionary* viewDict = NSDictionaryOfVariableBindings(scrollView, contentView, randomLabel, mainView);

[mainView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[contentView(==mainView)]" options:0 metrics:0 views:viewDict]];

There is a known issue (bug?) with multiline labels in scrollviews, that if you want it to resize according to the amount of text, you have to do some sleight of hand, such as:

dispatch_async(dispatch_get_main_queue(), ^{
randomLabel.preferredMaxLayoutWidth = self.view.bounds.size.width;
});

Programmatic UIScrollview with Autolayout

  1. You don't need to create a faux content view, you can add subviews directly to the scroll view (which I prefer). Apple does not recommend creating one, they only suggest that you can.

  2. Subviews of the scroll view shall not rely on the scroll view to determine their sizes, only their positions.

  3. Your constraints must define the left-most, right-most, top-most, and bottom-most edges in order for auto layout to create the content view for you.

When you create a scroll view, you may give its frame the bounds of the controller's view:

scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
scrollView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
scrollView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
scrollView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true

You must then set the boundaries of the content view by anchoring its subviews to the edges of the scroll view. To achieve vertical-only scrolling, your top-most view must be anchored to the top of the scroll view and none of the subviews anchored to the leading and trailing edges must exceed the width of the scroll view.

topMostView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(topMostView)
topMostView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
topMostView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
topMostView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
topMostView.heightAnchor.constraint(equalToConstant: 1000).isActive = true

Notice the topMostView does not rely on the scroll view to determine its size, only its position. The content in your scroll view now has a height of 1000 but it won't scroll because nothing is anchored to the bottom of the scroll view. Therefore, do that in your bottom-most view.

bottomMostView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(bottomMostView)
bottomMostView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
bottomMostView.topAnchor.constraint(equalTo: topMostView.bottomAnchor).isActive = true
bottomMostView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
bottomMostView.heightAnchor.constraint(equalToConstant: 1000).isActive = true

bottomMostView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true

The last anchor may seem odd because you're anchoring a view that is 1,000 points tall to an anchor that you just anchored to the bottom of the view which is definitely less than 1,000 points tall. But this is how Apple wants you to do it. By doing this, you do not need to create a content view, auto layout does it for you.

Defining the "edge constraints" (left-most, right-most, top-most, bottom-most) goes beyond scroll views. When you create a custom UITableViewCell, for example, using auto layout, defining the four edge constraints (i.e. where the top-most subview is anchored to the top of the cell topMostView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true, the bottom-most subview to the bottom of the cell bottomMostView.topAnchor.constraint(equalTo: self.bottomAnchor).isActive = true, etc.) is how you create self-sizing cells. Defining the edge constraints is how you create any self-sizing view, really.

Embedding lots of views into a uiscrollview and have constraints match up? Is this possible?

I figured this out. I was like "Well, I'll just embed the main View Controller's view into a subview", but nope, that's not allowed.

So I copied and pasted the head view into itself, deleted the originals, then embedded that one-layer-deep into a UIScrollView and manually set the size back and origin back to 40 pixels less each side, and 0,0 for x, y.

I have to re-hook up all the outlets but this is much easier than anticipated.

As a side note, it's still impossible to view [right side of] asset catalog on an air (without toggling off all sidebars), but it's not impossible to quickly do keyboard add/edits.

UIPageViewController and AutoLayout: constraints are not applied correctly

I had to add constraints programmatically to fix the problem. Note that I couldn't have added the auto layout constraints using IB because here I'm dealing with two views belonging to two different view-controllers in IB. The views are added programmatically as subviews because they are views of a UIPageViewController.

    override func viewDidLayoutSubviews() {
self.pageViewController.view.setTranslatesAutoresizingMaskIntoConstraints(false)

// Equal height constraint
let constraint = NSLayoutConstraint(item: self.mainImageView!, attribute: .Height, relatedBy: .Equal, toItem: self.pageViewController.view, attribute: .Height, multiplier: 1.0, constant: 0)
self.view.addConstraint(constraint)

// Equal width constraint
let constraint1 = NSLayoutConstraint(item: self.mainImageView!, attribute: .Width, relatedBy: .Equal, toItem: self.pageViewController.view, attribute: .Width, multiplier: 1.0, constant: 0)
self.view.addConstraint(constraint1)

// Equal top constraint
let constraint2 = NSLayoutConstraint(item: self.mainImageView!, attribute: .Top, relatedBy: .Equal, toItem: self.pageViewController.view, attribute: .Top, multiplier: 1.0, constant: 0)
self.view.addConstraint(constraint2)

self.view.layoutSubviews()
}

UIScrollview Autolayout Issue

The following code snippet in the containing view controller also seems to solve the problem, without relying on explicit sizes:

- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
self.mainScrollView.contentOffset = CGPointZero;
}

It does reset the content offset to the origin, but it seems that so do the other answers.



Related Topics



Leave a reply



Submit