Uiscrollview with Dynamically Sized Content

UIScrollView with dynamically sized content

When adding multiple elements to a scroll view at run-time, you may find it much easier to use a UIStackView... when setup properly, it will automatically grow in height with each added object.

As a simple example...

1) Start by adding a UIScrollView (I gave it a blue background to make it easier to see). Constrain it to Zero on all 4 sides:

Sample Image

Note that we see the "red circle" indicating missing / conflicting constraints. Ignore that for now.

2) Add a UIView as a "content view" to the scroll view (I gave it a systemYellow background to make it easier to see). Constrain it to Zero on all 4 sides to the Content Layout Guide -- this will (eventually) define the scroll view's content size. Also constrain it equal width and equal height to the Frame Layout Guide:

Sample Image

Important Step: Select the Height constraint, and in the Size Inspector pane select the Placeholder - Remove at build time checkbox. This will satisfy auto-layout in IB during design time, but will allow the height of that view to shrink / grow as necessary.

3) Add a Vertical UIStackView to the "content view". Constrain it to Zero on all 4 sides. Configure its properties to Fill / Fill / 8 (as shown below):

Sample Image

4) Add an @IBOutlet connection to the stack view in your view controller class. Now, at run-time, as you add UI elements to the stack view, all of your "scrollability" will be handled by auto-layout.

Here is an example class:

class DynaScrollViewController: UIViewController {

@IBOutlet var theStackView: UIStackView!

override func viewDidLoad() {
super.viewDidLoad()

// local var so we can reuse it
var theLabel = UILabel()
var theImageView = UIImageView()

// create a new label
theLabel = UILabel()
// this gets set to false when the label is added to a stack view,
// but good to get in the habit of setting it
theLabel.translatesAutoresizingMaskIntoConstraints = false
// multi-line
theLabel.numberOfLines = 0
// cyan background to make it easy to see
theLabel.backgroundColor = .cyan
// add 9 lines of text to the label
theLabel.text = (1...9).map({ "Line \($0)" }).joined(separator: "\n")

// add it to the stack view
theStackView.addArrangedSubview(theLabel)

// add another label
theLabel = UILabel()
// multi-line
theLabel.numberOfLines = 0
// yellow background to make it easy to see
theLabel.backgroundColor = .yellow
// add 5 lines of text to the label
theLabel.text = (1...5).map({ "Line \($0)" }).joined(separator: "\n")

// add it to the stack view
theStackView.addArrangedSubview(theLabel)

// create a new UIImageView
theImageView = UIImageView()
// this gets set to false when the label is added to a stack view,
// but good to get in the habit of setting it
theImageView.translatesAutoresizingMaskIntoConstraints = false
// load an image for it - I have one named background
if let img = UIImage(named: "background") {
theImageView.image = img
}
// let's give the image view a 4:3 width:height ratio
theImageView.widthAnchor.constraint(equalTo: theImageView.heightAnchor, multiplier: 4.0/3.0).isActive = true

// add it to the stack view
theStackView.addArrangedSubview(theImageView)

// add another label
theLabel = UILabel()
// multi-line
theLabel.numberOfLines = 0
// yellow background to make it easy to see
theLabel.backgroundColor = .green
// add 2 lines of text to the label
theLabel.text = (1...2).map({ "Line \($0)" }).joined(separator: "\n")

// add it to the stack view
theStackView.addArrangedSubview(theLabel)

// add another UIImageView
theImageView = UIImageView()
// this gets set to false when the label is added to a stack view,
// but good to get in the habit of setting it
theImageView.translatesAutoresizingMaskIntoConstraints = false
// load a different image for it - I have one named AquariumBG
if let img = UIImage(named: "AquariumBG") {
theImageView.image = img
}
// let's give this image view a 1:1 width:height ratio
theImageView.heightAnchor.constraint(equalTo: theImageView.widthAnchor, multiplier: 1.0).isActive = true

// add it to the stack view
theStackView.addArrangedSubview(theImageView)

}

}

If the steps have been followed, you should get this output:

Sample Image

and, after scrolling to the bottom:

Sample Image

Dynamically adjust UIScrollView contentSize depending on the overall size of a UIView and UITextView

You are close... but you don't need to worry about setting the .contentSize of the scroll view. Let auto-layout handle that for you.

The key point you were missing was a bottom constraint for your scroll view's subviews.

Here is your class (with example title and body text), only slightly modified to work. The comments I added should make it clear:

class StoryViewController: UIViewController {

var storyTitle = String()
var storyBody = ""

let titleLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.numberOfLines = 0
label.font = UIFont(name: "Arial-BoldMT", size: 28)
label.textColor = UIColor.black
label.translatesAutoresizingMaskIntoConstraints = false

return label
}()

let textView: UITextView = {
let textView = UITextView()
textView.textAlignment = .left
textView.isEditable = false
textView.isSelectable = false
textView.font = UIFont(name: "ArialMT", size: 19)
textView.isScrollEnabled = false
textView.translatesAutoresizingMaskIntoConstraints = false

return textView
}()

let scrollView: UIScrollView = {
let scrollingView = UIScrollView()
scrollingView.translatesAutoresizingMaskIntoConstraints = false
return scrollingView
}()

override func viewDidLoad() {
super.viewDidLoad()

self.navigationController?.navigationBar.shadowImage = UIImage()
self.view.backgroundColor = UIColor.white

// sample Title and Body
storyTitle = "Example\nTitle Label"

storyBody = ""
storyBody += "UITextView:"
storyBody += "NL"
storyBody += "When a user taps a text view, a keyboard appears; when a user taps Return in the keyboard, the keyboard disappears and the text view can handle the input in an application-specific way. You can specify attributes, such as font, color, and alignment, that apply to all text in a text view."
storyBody += "NL"
storyBody += "UIScrollView:"
storyBody += "NL"
storyBody += "UIScrollView provides a mechanism to display content that is larger than the size of the application’s window and enables users to scroll within that content by making swiping gestures."
storyBody += "NL"
storyBody += "UILabel:"
storyBody += "NL"
storyBody += "A label can contain an arbitrary amount of text, but UILabel may shrink, wrap, or truncate the text, depending on the size of the bounding rectangle and properties you set. You can control the font, text color, alignment, highlighting, and shadowing of the text in the label."
storyBody += "NL"
storyBody += "UIButton:"
storyBody += "NL"
storyBody += "You can set the title, image, and other appearance properties of a button. In addition, you can specify a different appearance for each button state."

titleLabel.text = storyTitle
textView.text = storyBody.replacingOccurrences(of: "NL", with: "\n\n")

// you do not need either of these two lines
//textView.sizeToFit()
//scrollView.contentSize = CGSize(width: self.view.frame.width, height: textView.contentSize.height)

view.addSubview(scrollView)

scrollView.addSubview(titleLabel)
scrollView.addSubview(textView)

NSLayoutConstraint.activate([

scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),

// don't use .centerXAnchor constraints
//titleLabel.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
//textView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),

// don't need to set titleLabel bottom constraint
//titleLabel.bottomAnchor.constraint(equalTo: textView.topAnchor, constant: -16),

titleLabel.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 16),
titleLabel.leadingAnchor.constraint(equalTo: scrollView.readableContentGuide.leadingAnchor),
titleLabel.trailingAnchor.constraint(equalTo: scrollView.readableContentGuide.trailingAnchor),

textView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 16),
textView.leadingAnchor.constraint(equalTo: scrollView.readableContentGuide.leadingAnchor),
textView.trailingAnchor.constraint(equalTo: scrollView.readableContentGuide.trailingAnchor),

// set the textView's bottomAnchor to let auto-layout determine content size
textView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),

])

}
}

UIScrollView with dynamic content

The contentSize needs to be the size of content contained within the scroll view.

If the contentSize is wider than the bounds.size, then you can scroll left and right. If the contentSize is taller than the bounds.size, then you can scroll up and down.

You need to set the contentSize to be the entire area you wish to contain within your scroll view.

UITextField *textField = [[UITextField alloc]initWithFrame:CGRectMake(textField.frame.origin.x, textField.frame.origin.y + textField.frame.size.height+5, textField.frame.size.width, textField.frame.size.height)];
textField.placeholder = @"Add more text";
textField.borderStyle = UITextBorderStyleNone;
textField.font = [UIFont systemFontOfSize:14];
textField.autocorrectionType = UITextAutocorrectionTypeYes;
textField.clearButtonMode = UITextFieldViewModeWhileEditing;
textField.autocapitalizationType = UITextAutocapitalizationTypeSentences;
textField.returnKeyType = UIReturnKeyDone;
textField.tag = textFieldTag;
textField.delegate = self;

[self.scrollView addSubview:textField];

// Update the contentSize to include the new text field.
CGFloat width = self.scrollView.bounds.size.width;
CGFloat height = CGRectGetMaxY(textField.frame);
self.scrollView.contentSize = CGSizeMake(width, height);

NOTES:

  • Don't start variables or methods with new. It has special meaning and you will confuse other Objective-C developers and/or the compiler.
  • textField.tag = … is the same as [textfield setTag:…]; You seem to like the dot syntax in other places, so I switched to that.
  • I'm assuming you don't want the scroll view to pan left and right, so I pinned the content width to the scroll view's width.

iOS: Create UIScrollView with dynamic contentSize in Interface Builder

The following did the trick:

"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."

This tutorial should help you out

Setting Dynamic Height to UIScrollView Swift

We can use auto layout, where we use a content view in scrollview, which can be pinned to the scrollview and the height and width constraints given equal to the main view. Then if we want the height to vary dynamically we can give height constraint a low priority compared to the others and hence the content view's height will increase based on its intrinsic size, because of its sub views.

Reference Links:

1) https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/AutolayoutPG/WorkingwithScrollViews.html#//apple_ref/doc/uid/TP40010853-CH24-SW1

2) https://www.youtube.com/watch?v=6J22gHORk2I

iOS: UIScrollView with dynamic content using ContainerView (step by step)

I am sure this has been answered before but I cannot remember if it has been answered down to why people have so much trouble with a scrollview. It comes down to 2 things that you have to know about UIScrollView and Autolayout.

1) The scrollview needs to be able to calculate the width and height of the content that is inside. This helps with deciding if it actually needs to scroll.

2) Some views have a size based on the content inside. Example a UILabel has an "intrinsic" content size. That means if you drag it out onto the storyboard you do not need to set a height or width unless you are trying to constraint it someway. Other examples of views with intrinsic sizes would be a UIButton, UIImageView(if it has an image), UITextView(with scrolling disabled) and other controls that may have text or images in them.

So let's start really simple and drag a UIScrollView onto the storyboard and pin it to the superview. All is good no warnings.
Start

Now drag a UILabel onto the scrollview and take a quick peak at the warnings. ScrollView ambiguous scrollable content(width and height).

UILabelNoConstraints

Now add a top, bottom,leading, and trailing of 20 for all.

No warnings.

UILabelConstraints

Test 2)
Delete the UILabel and drag a UIView onto the scrollview and add top,bottom,leading,and trailing of say 20.

Warning! Warning! Warning!

UIViewWarnings

The problem is that a UIView does not have the ability to size itself and the scrollview does not know how big its content will be so it cannot setup.


IF THIS MAKES SENSE == True
continue
ELSE
goBack

Now here is where it will get more complex depending on how far we go but the concepts above govern the entire process.

Investigating your project and setup you did pretty well to be learning UIScrollview.

Now lets go over your summary and I will comment in line as to some things.

From your quote above

"From this we can conclude:
The position of the ContainerView is correct (i.e. between SecondLabel and BottomLabel), but the BottomLabel does not adhere its constraint to be below the ContainerView."

***-> You summary here is actually incorrect. Go to the container above the label and checkmark clip to bounds in interface builder and re run the project and the label will be at the bottom but there will be no green view. Why? It does not know how big it is supposed to be. When you ran it before it loaded best it could and uilabels are clear so when it went outside its bounds it looked like it was not correct.

"The TableView's height is obviously 0. This can also be seen since func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) is not called. If we set a height constraint on the TableView, items will show up."

***-> This is the trickiest to deal with. You would have to load the tableview and get the tableview content size and update a height constraint on the tableview in code to get it to update for the scrollview to resize for the container that is holding the controller.

An alternative would be a stackview that can determine it's height and width if the content it holds has intrinsic content size.

But back to the tableview. You would need to set a >= 40 on the tableview//your row height to start on the dynamic view. After you check your datasource if the count is 0 you would update the constraint to 0 and use a delegate to let the scrollview know to update it's constraint on the dynamicviewcontroller to not show the table. I hope this makes sense. Then conversely if the count is say 10 items in the datasource update the constraint on both the dynamicviewcontroller tableview height constraint to 10 * 40 like so

import UIKit

class DynamicEmbeddedViewController : UIViewController, UITableViewDataSource, UITableViewDelegate
{
@IBOutlet weak var tableViewConstraint: NSLayoutConstraint!
@IBOutlet weak var tableView: UITableView!

let data = ["First", "Second", "Third", "Fourth", "Fifth", "Sixth", "Last"]

override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "TableViewCell")

//resize our constraint
let count = data.count
let constant = count * 40
tableViewConstraint.constant = CGFloat(constant)
self.updateViewConstraints()
//in a real app a delegate call back would be good to update the constraint on the scrollview
}

And in the first controller it would look like this.

import UIKit

class FirstViewController: UIViewController {

@IBOutlet weak var containerViewHeightConstraint: NSLayoutConstraint!
@IBOutlet weak var containerView: UIView!
@IBOutlet weak var scrollView: UIScrollView!
var dynamicView : DynamicEmbeddedViewController?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.

if dynamicView != nil{
dynamicView?.tableView.reloadData()
//get size
let size = dynamicView?.tableView.contentSize.height
//cheating on the 300 because i see you set the other views in that controller at 150 each
containerViewHeightConstraint.constant = size! + 300
self.view.updateConstraintsIfNeeded()
}
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if (segue.identifier == "ContainerViewSegue") {
dynamicView = segue.destination as? DynamicEmbeddedViewController
}
}

}

Final
You can see we resize everything to fit. Other than that you got it right. Now to production worthy code. Most times you would know what content would be shown so you can control it. Another way is to use UITableView or UICollectionView for all the content and have different cells that would load based on the content. I hope this post clears it up a bit. I am sure we could continue to add to it but hopefully the concepts covered will be enough. If this answers your questions please show some love for the time it takes to do this. I can also upload this to GitHub if you like but it might be best to go in the repo you started.

Dynamic sized UIImageView in UIScrollView with some complex conditions

This might do the trick for you...

Kinda tough to explain everything, but you should be able to follow the constraints in the image. The only thing you cannot see is that the multi-line label must have its Content Compression Resistance Priority set to Required (1000).

The idea is to set constraints with specific Priority values so auto-layout will try to make the image view width - at 998 Priority - as wide as the scroll view, but allow it to shrink to a minimum width of 70% as the content below it grows. When it reaches 70%, it will not get any smaller, and the combined heights of the image view + label + button will force the "content" view to expand vertically, as it has a height constraint of 999.

Here the label has 7 lines of text, with a font size of 18:

Sample Image

And here's how it looks with the font size increased to 28 - notice that the image view is now less-than the full width of the scroll view:

Sample Image

and now, with the font size increased to 36, we see that the image view is at its min width of 70%, and the button has been pushed down past the bottom of the scroll view's frame (so everything will scroll):

Sample Image

Here's the source for the Storyboard - play around with the font-size and/or the amount of text in the label to see it work:

<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="eB4-FG-006">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="cKl-8C-enU">
<objects>
<viewController id="eB4-FG-006" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="qMY-Qc-gio">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="q6G-7e-kz1">
<rect key="frame" x="20" y="60" width="335" height="567"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="mbq-g1-m6Z" userLabel="ContentView">
<rect key="frame" x="0.0" y="0.0" width="335" height="567"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="N7z-eO-cci">
<rect key="frame" x="0.0" y="0.0" width="335" height="335"/>
<color key="backgroundColor" red="0.0" green="0.97680455450000003" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="width" secondItem="N7z-eO-cci" secondAttribute="height" multiplier="1:1" id="3Za-Q8-IFq"/>
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="1000" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Pp5-EQ-8Vh" userLabel="Multi-LineLabel">
<rect key="frame" x="142.5" y="343" width="50" height="150.5"/>
<color key="backgroundColor" red="0.99953407049999998" green="0.98835557699999999" blue="0.47265523669999998" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<string key="text">Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7</string>
<fontDescription key="fontDescription" type="system" pointSize="18"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="1000" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="rg8-I7-3hZ">
<rect key="frame" x="144.5" y="501.5" width="46" height="30"/>
<color key="backgroundColor" red="1" green="0.83234566450000003" blue="0.47320586440000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="30" id="NZU-hx-0M5"/>
</constraints>
<state key="normal" title="Button"/>
</button>
</subviews>
<color key="backgroundColor" red="1" green="0.14913141730000001" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="rg8-I7-3hZ" firstAttribute="top" secondItem="Pp5-EQ-8Vh" secondAttribute="bottom" constant="8" id="6c5-vJ-c8B"/>
<constraint firstItem="rg8-I7-3hZ" firstAttribute="centerX" secondItem="mbq-g1-m6Z" secondAttribute="centerX" id="C6W-uG-Irk"/>
<constraint firstItem="Pp5-EQ-8Vh" firstAttribute="centerX" secondItem="mbq-g1-m6Z" secondAttribute="centerX" id="MDd-eI-O2X"/>
<constraint firstItem="Pp5-EQ-8Vh" firstAttribute="top" secondItem="N7z-eO-cci" secondAttribute="bottom" constant="8" id="Ozv-Hn-EbX"/>
<constraint firstItem="N7z-eO-cci" firstAttribute="top" secondItem="mbq-g1-m6Z" secondAttribute="top" id="Qbn-EK-zhn"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="rg8-I7-3hZ" secondAttribute="bottom" constant="8" id="XRT-bz-2bI"/>
<constraint firstItem="rg8-I7-3hZ" firstAttribute="top" secondItem="Pp5-EQ-8Vh" secondAttribute="bottom" constant="8" id="bny-1w-xOv"/>
<constraint firstItem="N7z-eO-cci" firstAttribute="width" relation="greaterThanOrEqual" secondItem="mbq-g1-m6Z" secondAttribute="width" multiplier="0.7" id="bsP-M1-7uP"/>
<constraint firstItem="N7z-eO-cci" firstAttribute="width" secondItem="mbq-g1-m6Z" secondAttribute="width" priority="998" id="fr8-4U-00h"/>
<constraint firstItem="N7z-eO-cci" firstAttribute="centerX" secondItem="mbq-g1-m6Z" secondAttribute="centerX" id="hVh-Ld-mDS"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" red="0.0" green="0.47843137250000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="mbq-g1-m6Z" secondAttribute="trailing" id="Fbg-Dx-idJ"/>
<constraint firstAttribute="bottom" secondItem="mbq-g1-m6Z" secondAttribute="bottom" id="Hrx-mL-t1p"/>
<constraint firstItem="mbq-g1-m6Z" firstAttribute="top" secondItem="q6G-7e-kz1" secondAttribute="top" id="La2-Bo-p6J"/>
<constraint firstItem="mbq-g1-m6Z" firstAttribute="leading" secondItem="q6G-7e-kz1" secondAttribute="leading" id="S8w-du-yf0"/>


Related Topics



Leave a reply



Submit