iOS Autolayout with Uiscrollview: Why Does Content View of Scroll View Not Fill the Scroll View

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;
});

ScrollView Content not filling when stack view is embedded in it

Setting StackView constraints to content layout guide of scrollView instead of ScrollView itself, and setting equal width constraint of StackView and solved the problem for me. Screenshots of steps are attached below

  1. Attached top, leading and trailing constraints to content layout guide

Sample Image


  1. Set equal width constraints to stack view and parent scrollView

Sample Image

UIScrollView Subviews not expanding to fill width (Autolayout)

The reason for this is that the contentView of the UIScrollView still doesn't know that you want it to take up the width of it's parentView.

You can fix this by adding the following constraint in iOS11:

self.containerView.contentLayoutGuide.widthAnchor.constraint(equalTo: self.view.widthAnchor).isActive = true

This says "Hey, I want you to lock the content Width to the width of the superview.

Pre iOS 11 you can simply constrain a subview to both the parent view's left and right anchors AND the content view's left and right anchors.

Like so:

self.wtfView.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 40.0).isActive = true
self.wtfView.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: 40.0).isActive = true

Much like, Aleksei's recommendation you are now constraining the width to a rigid value ( the width of the parent view ), and the scrollview will use that to decide the width of the scrollview.

My Swift 4 UIScrollView with autolayout constraints is not scrolling

You can do this with Auto Layout. The secret is to constrain the edges of the containerView to the edges of the scrollView. It's not intuitive, but constraining the edges of the containerView doesn't set the size, it just makes sure that the content size of the scrollView grows as the containerView grows. By setting constraints for the width of the containerView to a constant that is a larger number than the width of the scrollView, the content will scroll horizontally.

Note: When configuring a scrollView this way, you do not set the contentSize of the scrollView. The contentSize will be computed for you by Auto Layout and it will be equal to the size of the containerView. It is important to make sure that the size of the containerView is fully specified by the constraints.

Here's what I changed to make it work:

containerView = UIView()
containerView.backgroundColor = #colorLiteral(red: 0.176470592617989, green: 0.498039215803146, blue: 0.756862759590149, alpha: 1.0)
scrollView.addSubview(containerView)
//containerView.frame = CGRect(x: 0, y: 0, width: 1080, height: 200)
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.topAnchor.constraint(equalTo:scrollView.topAnchor).isActive = true
containerView.leadingAnchor.constraint(equalTo:scrollView.leadingAnchor).isActive = true
containerView.trailingAnchor.constraint(equalTo:scrollView.trailingAnchor).isActive = true
containerView.bottomAnchor.constraint(equalTo:scrollView.bottomAnchor).isActive = true
containerView.heightAnchor.constraint(equalToConstant: 200).isActive = true
containerView.widthAnchor.constraint(equalToConstant: 1080).isActive = true

Why isn't my content scrolling?

For it to scroll, the containerView must be larger than the scrollView. Your error is that you have set the constraints such that the containerView is the same width and height as the scrollView, and that is why your content isn't scrolling.

If you want it to scroll horizontally, the width of the containerView must be larger than the scrollView's width. You can do this in one of two ways:

  1. Specify an explicit constant width for the containerView that is larger than the scrollView's width.

    OR

  2. Chain the subviews of the containerView from left to right with the left most being constained to the leading edge of the containerView. Fully specify the widths of the subviews, and place distance contraints between the subviews. The rightmost subview must have an offset from the trailing edge of the containerView. By doing this, Auto Layout can compute the width of the containerView and set the contentSize of the scrollView.


Mini project: update

This is a version of your mini project which uses a chain of constrained views to define the containerView's width. The key is the final constraint after the for loop in viewDidLoad() which connects the last button's trailingAnchor (aka startPoint) to the containerView's trailingAnchor. This completes the chain of contraints and buttons which connect the leading edge of the containerView with the trailing edge of containerView. With this, Auto Layout is able to compute the width of the containerView and establish the contentSize of the scrollView.

import UIKit
import PlaygroundSupport

class FilterViewController: UIViewController {
var filterView: UIView!
var scrollView: UIScrollView!
var containerView: UIView!

override func loadView() {
filterView = UIView()
view = filterView
view.backgroundColor = #colorLiteral(red: 0.909803926944733, green: 0.47843137383461, blue: 0.643137276172638, alpha: 1.0)

scrollView = UIScrollView()
scrollView.backgroundColor = #colorLiteral(red: 0.474509805440903, green: 0.839215695858002, blue: 0.976470589637756, alpha: 1.0)
view.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 40).isActive = true
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
scrollView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
scrollView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.25).isActive = true
scrollView.isScrollEnabled = true

containerView = UIView()
containerView.backgroundColor = #colorLiteral(red: 0.176470592617989, green: 0.498039215803146, blue: 0.756862759590149, alpha: 1.0)
scrollView.addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false

// This is key: connect all four edges of the containerView to
// to the edges of the scrollView
containerView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
containerView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
containerView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
containerView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true

// Making containerView and scrollView the same height means the
// content will not scroll vertically
containerView.heightAnchor.constraint(equalTo: scrollView.heightAnchor).isActive = true
}

class Buttons {
let button = UIButton()
init(titleText: String) {
button.backgroundColor = #colorLiteral(red: 0.976470589637756, green: 0.850980401039124, blue: 0.549019634723663, alpha: 1.0)
button.setTitle(titleText, for: .normal)
}
}

override func viewDidLoad() {
super.viewDidLoad()

let b1 = Buttons(titleText: "one")
let b2 = Buttons(titleText: "two")
let b3 = Buttons(titleText: "three")
let b4 = Buttons(titleText: "four")
let b5 = Buttons(titleText: "five")
let buttonArray = [b1, b2, b3, b4, b5]
var startPoint = containerView.leadingAnchor
for btn in buttonArray {
let theBtn = btn.button
containerView.addSubview(theBtn)
theBtn.translatesAutoresizingMaskIntoConstraints = false
theBtn.leadingAnchor.constraint(equalTo: startPoint, constant: 20).isActive = true
theBtn.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
theBtn.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true
theBtn.widthAnchor.constraint(equalTo: theBtn.heightAnchor).isActive = true
startPoint = theBtn.trailingAnchor
}
// Complete the chain of constraints
containerView.trailingAnchor.constraint(equalTo: startPoint, constant: 20).isActive = true
}
}

let filterViewController = FilterViewController()
PlaygroundPage.current.liveView = filterViewController

UIScrollView with AutoLayout does not scroll

Your logoImageView is constrained to the wrong place...

// constrain Top to scrollView contentLayoutGuide Top
//logoImageView.topAnchor.constraint(equalTo: scrollView.safeAreaLayoutGuide.topAnchor, constant: 16),
logoImageView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: 16),

Scroll view that contains a stack view (and/or a content view) not working expectedly

Here's an example that will work - see if you can figure out what you may have done differently.

Instead of using a "content view" we'll just add the stack view directly to the scroll view via code.

Here's the Storyboard layout:

Sample Image

Here's the source for the Storyboard, so you can examine it directly:

<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="dVO-AO-rAX">
<device id="retina3_5" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Kevvv View Controller-->
<scene sceneID="e7x-2X-Pdg">
<objects>
<viewController id="dVO-AO-rAX" customClass="KevvvViewController" customModule="MiniScratch" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="ZMq-2S-yNo">
<rect key="frame" x="0.0" y="0.0" width="320" height="480"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="bEj-BB-5lU">
<rect key="frame" x="0.0" y="44" width="320" height="402"/>
<viewLayoutGuide key="contentLayoutGuide" id="VmC-Gj-CCr"/>
<viewLayoutGuide key="frameLayoutGuide" id="HBJ-Ua-m26"/>
</scrollView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="bEj-BB-5lU" firstAttribute="leading" secondItem="goZ-oS-cQl" secondAttribute="leading" id="Jwq-Tg-wRK"/>
<constraint firstItem="goZ-oS-cQl" firstAttribute="bottom" secondItem="bEj-BB-5lU" secondAttribute="bottom" constant="34" id="bHJ-DL-1xi"/>
<constraint firstItem="bEj-BB-5lU" firstAttribute="trailing" secondItem="goZ-oS-cQl" secondAttribute="trailing" id="gIL-OY-ENf"/>
<constraint firstItem="bEj-BB-5lU" firstAttribute="top" secondItem="goZ-oS-cQl" secondAttribute="top" constant="44" id="zAh-qk-82E"/>
</constraints>
<viewLayoutGuide key="safeArea" id="goZ-oS-cQl"/>
</view>
<connections>
<outlet property="scrollView" destination="bEj-BB-5lU" id="jYI-Wh-d6w"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="ieG-NN-t0K" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="136.875" y="105"/>
</scene>
</scenes>
</document>

And here's example code that will add a stack view to the scroll view, add 40 labels to the stack view, and then properly constrain the stack view to the scroll view:

class KevvvViewController: UIViewController {

@IBOutlet var scrollView: UIScrollView!

override func viewDidLoad() {
super.viewDidLoad()

let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 12

stack.translatesAutoresizingMaskIntoConstraints = false

scrollView.addSubview(stack)

for i in 1...40 {
let v = UILabel()
v.backgroundColor = .yellow
v.text = "Label \(i)"
stack.addArrangedSubview(v)
}

NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
stack.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
stack.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
stack.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),

stack.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor),
])

// to make it easy to see the scroll view frame
scrollView.backgroundColor = .cyan
}

}

Result, after scrolling down to the 17th label (iPhone 8):

Sample Image



Related Topics



Leave a reply



Submit