I have a view with 3 dynamic labels inside it and I am trying to find a way to centre it vertically in a scroll view but when its dynamic labels are too large to fit on a pa
You can accomplish this by embedding the labels in a stack view and embedding the stack view in a UIView. The label text will expand the stack view vertically, which will expand the content view vertically, which will control the scroll view's .contentSize
.
Black is the scroll view; blue is the content view; stack view only shows as thin gray outline; labels are yellow, green and cyan. The background colors just make it easier to see what's what.
Bunch of steps, but should be clear:
0
for top/leading/trailing/bottom of contentView to scrollViewVertical / Fill / Fill / Spacing: 20
>= 8
Setting the height Priority of the contentView to 250 will allow it to expand vertically based on the text in the labels.
Setting top and bottom stackView constraints to >= 8 will "push" the top and bottom of the contentView, but allow extra space when you don't have enough text to exceed the vertical bounds.
Results:
Here's a storyboard with everything in place for reference:
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14109" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="SeU-GX-TTY">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
<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="bCz-Kd-LLi">
<objects>
<viewController id="SeU-GX-TTY" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="qjW-fW-J5n">
<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="Zj2-9M-SP5" userLabel="scrollView">
<rect key="frame" x="0.0" y="40" width="375" height="627"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Pmb-IH-ckB" userLabel="contentView">
<rect key="frame" x="0.0" y="0.0" width="375" height="627"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="EfQ-93-hcI" userLabel="stackView">
<rect key="frame" x="40" y="164" width="295" height="299.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="Anger" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Sxz-f7-zjR" userLabel="topLabel">
<rect key="frame" x="0.0" y="0.0" width="295" height="43"/>
<color key="backgroundColor" red="0.99953407049999998" green="0.98835557699999999" blue="0.47265523669999998" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="36"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="STy-4u-e1W" userLabel="centerLabel">
<rect key="frame" x="0.0" y="63" width="295" height="183"/>
<color key="backgroundColor" red="0.83216959239999999" green="0.98548370600000001" blue="0.47333085539999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<string key="text">Anger is an intense emotion defined as a response to a perceived provocation, the invasion of one's boundaries, or a threat. From an evolutionary standpoint, anger servers to mobilise psychological resources in order to address the threat/invasion. Anger is directed at an individual of equal status.</string>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="749" text="Based on information from Wikipedia. APA DIctionary of Psycology" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="u3i-zP-e1M" userLabel="bottomLabel">
<rect key="frame" x="0.0" y="266" width="295" height="33.5"/>
<color key="backgroundColor" red="0.45138680930000002" green="0.99309605359999997" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" red="0.46202266219999999" green="0.83828371759999998" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="EfQ-93-hcI" secondAttribute="trailing" constant="40" id="4HE-oJ-RE3"/>
<constraint firstItem="EfQ-93-hcI" firstAttribute="centerY" secondItem="Pmb-IH-ckB" secondAttribute="centerY" id="H9O-jj-a7A"/>
<constraint firstItem="EfQ-93-hcI" firstAttribute="top" relation="greaterThanOrEqual" secondItem="Pmb-IH-ckB" secondAttribute="top" constant="8" id="cKe-DN-Lbn"/>
<constraint firstItem="EfQ-93-hcI" firstAttribute="leading" secondItem="Pmb-IH-ckB" secondAttribute="leading" constant="40" id="f4g-6a-VqH"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="EfQ-93-hcI" secondAttribute="bottom" constant="8" id="meR-gT-OVG"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="Pmb-IH-ckB" firstAttribute="top" secondItem="Zj2-9M-SP5" secondAttribute="top" id="HCI-bq-7ur"/>
<constraint firstAttribute="trailing" secondItem="Pmb-IH-ckB" secondAttribute="trailing" id="Tdl-c0-GAV"/>
<constraint firstItem="Pmb-IH-ckB" firstAttribute="width" secondItem="Zj2-9M-SP5" secondAttribute="width" id="Zj9-ND-Fqt"/>
<constraint firstItem="Pmb-IH-ckB" firstAttribute="leading" secondItem="Zj2-9M-SP5" secondAttribute="leading" id="ckv-wi-E1z"/>
<constraint firstItem="Pmb-IH-ckB" firstAttribute="height" secondItem="Zj2-9M-SP5" secondAttribute="height" priority="250" id="jpK-HZ-vva"/>
<constraint firstAttribute="bottom" secondItem="Pmb-IH-ckB" secondAttribute="bottom" id="psz-UW-bNp"/>
</constraints>
</scrollView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="Zj2-9M-SP5" firstAttribute="top" secondItem="Xr7-LW-bbC" secondAttribute="top" constant="20" id="EgA-Bk-3fC"/>
<constraint firstItem="Zj2-9M-SP5" firstAttribute="leading" secondItem="qjW-fW-J5n" secondAttribute="leading" id="MBG-pL-R8Q"/>
<constraint firstItem="Xr7-LW-bbC" firstAttribute="bottom" secondItem="Zj2-9M-SP5" secondAttribute="bottom" id="e9K-6A-Y9F"/>
<constraint firstItem="Xr7-LW-bbC" firstAttribute="trailing" secondItem="Zj2-9M-SP5" secondAttribute="trailing" id="yfs-wt-Br8"/>
</constraints>
<viewLayoutGuide key="safeArea" id="Xr7-LW-bbC"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="lHx-xL-Vx5" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="225" y="106"/>
</scene>
</scenes>
</document>
And here's a quick example replicating that layout / functionality via code only:
//
// ScrollWorkViewController.swift
//
// Created by DonMag on 6/12/19.
//
import UIKit
class ScrollWorkViewController: UIViewController {
let theScrollView: UIScrollView = {
let v = UIScrollView()
v.backgroundColor = .red
return v
}()
let contentView: UIView = {
let v = UIView()
v.backgroundColor = UIColor(red: 0.25, green: 0.25, blue: 1.0, alpha: 1.0)
return v
}()
let stackView: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.alignment = .fill
v.distribution = .fill
v.spacing = 20
return v
}()
let topLabel: UILabel = {
let v = UILabel()
v.font = UIFont.boldSystemFont(ofSize: 32.0)
v.backgroundColor = .yellow
return v
}()
let centerLabel: UILabel = {
let v = UILabel()
v.font = UIFont.systemFont(ofSize: 17.0)
v.numberOfLines = 0
v.backgroundColor = .green
return v
}()
let bottomLabel: UILabel = {
let v = UILabel()
v.font = UIFont.systemFont(ofSize: 14.0)
v.numberOfLines = 0
v.backgroundColor = .cyan
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
[theScrollView, contentView, stackView, topLabel, centerLabel, bottomLabel].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
view.addSubview(theScrollView)
theScrollView.addSubview(contentView)
contentView.addSubview(stackView)
stackView.addArrangedSubview(topLabel)
stackView.addArrangedSubview(centerLabel)
stackView.addArrangedSubview(bottomLabel)
let contentViewHeightConstraint = contentView.heightAnchor.constraint(equalTo: theScrollView.heightAnchor, constant: 0.0)
contentViewHeightConstraint.priority = .defaultLow
NSLayoutConstraint.activate([
// constrain all 4 sides of the scroll view to the safe area
theScrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0.0),
theScrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0.0),
theScrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 0.0),
theScrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 0.0),
// constrain all 4 sides of the content view to the scroll view
contentView.topAnchor.constraint(equalTo: theScrollView.topAnchor, constant: 0.0),
contentView.bottomAnchor.constraint(equalTo: theScrollView.bottomAnchor, constant: 0.0),
contentView.leadingAnchor.constraint(equalTo: theScrollView.leadingAnchor, constant: 0.0),
contentView.trailingAnchor.constraint(equalTo: theScrollView.trailingAnchor, constant: 0.0),
// constrain width of content view to width of scroll view
contentView.widthAnchor.constraint(equalTo: theScrollView.widthAnchor, constant: 0.0),
// constrain the stack view >= 8-pts from the top
// <= minus 8-pts from the bottom
// 40-pts leading and trailing
stackView.topAnchor.constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: 8.0),
stackView.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -8.0),
stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 40.0),
stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -40.0),
// constrain stack view centerY to contentView centerY
stackView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0.0),
// activate the contentView's height constraint
contentViewHeightConstraint,
])
topLabel.text = "Anger"
bottomLabel.text = "Based on information from Wikipedia APA Dictionary of Psychology"
// a sample paragraph of text
let centerSampleText = "Anger is an intense emotion defined as a response to a perceived provocation, the invasion of one’s boundaries, or a threat. From an evolutionary standpoint, anger servers to mobilise psychological resources in order to address the threat/invasion. Anger is directed at an individual of equal status."
// change to repeat the center-label sample text
let numberOfParagraphs = 2
var s = ""
for i in 1...numberOfParagraphs {
s += "\(i). " + centerSampleText
if i < numberOfParagraphs {
s += "\n\n"
}
}
centerLabel.text = s
}
}
I've faced the same issue while trying to vertically center a label inside a scrollView when the label does not fill the scrollView (top aligned otherwise) and found an as simple solution as it is dirty, all in IB with no extension or sub-classing.
First of all, set the usual contraints when dealing with a UIScrollView:
Then make the child view's height greater or equal to the scrollView height (it triggers a constraint error). Now set this constraint multiplier to anything close to 1 but not quite (eg. 0.999999). The constraint error disappears and the child view behaves as expected.
Not proud of this one but it could save some time to people in a rush.
To add on to DonMag's answer, you can actually accomplish the exact same thing by using only the UIScrollView
and UIStackView
. This is for iOS 11 and above as it uses the contentLayoutGuide
and frameLayoutGuide
properties on UIScrollView
.
When I refer to scroll view's content view, it simply means the scrollable content layout area within the scroll view. It refers to the content layout guide, not the stack view.
The steps are as follows, written in pseudocode (just add the equivalent constraints):
scrollView.contentLayoutGuide.height >= scrollView.frameLayoutGuide.height
- This sets up the scroll view content to be at least as tall as the scroll view itself. By doing this on its own, we aren't able to center the content just quite yet.stackView.centerY == scrollView.contentLayoutGuide.centerY
- This will force the stack view to be centered vertically with the scroll view content view. But wait, what if the stack view is too short? Remember in step 1, we forced the content size to be at least as tall as the scroll view itself. This means that if the stack view is not tall enough to cause scrolling, it will actually center itself within the content view, which is as tall as the scroll view, causing the desired effect.stackView.top/bottom <= (inside) scrollView.contentLayoutGuide.top/bottom
- This just sets the stack view's edges so that the top and bottom are within the scroll view's content view.stackView.top/bottom == (inside) scrollView.contentLayoutGuide.top/bottom
with defaultLow
priority - This forces the scroll view's content view to have intrinsic height in the cases where your view debugger complains.This should be sufficient for vertical constraints. Add your necessary horizontal constraints, and everything should be good to go!