Center UIView vertically in scroll view when its dynamic Labels are small enough, but align it to the top once they are not
Asked Answered
V

4

27

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 page, make the text start from the top. What Xcode is doing at the moment is this:

enter image description here

What I am trying to do is this:

enter image description here

Any ideas about how to achieve this? Thanks.

Vagabond answered 8/6, 2018 at 18:21 Comment(0)
E
75

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.

enter image description here

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:

  • add a scrollView, set constraints as normal
  • add a UIView to scrollView - name it "contentView"
  • set constraints 0 for top/leading/trailing/bottom of contentView to scrollView
  • set width and height of contentView equal to width and height of scrollView
  • add a stackView to contentView
  • set stackView to Vertical / Fill / Fill / Spacing: 20
  • set stackView constraints top: 8, bottom: 8, leading: 40, trailing: 40 to contentView
  • set stackView centerY constraint to contentView
  • add three labels to stackView
  • set fonts and text, set number of lines = 0 for center and bottom labels
  • change stackView top and bottom constraints to >= 8
  • change contentView height constraint to Priority: 250
  • I think that's everything....

enter image description here

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:

enter image description here enter image description here

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

Edit - since this answer still gets occasional "up-votes," I've updated the code to reflect the more modern usage of scroll view .contentLayoutGuide and .frameFlayoutGuide. Also added buttons to interactively add / remove text demonstrate the centering.

class ScrollWorkViewController: UIViewController {
    
    let theScrollView: UIScrollView = {
        let v = UIScrollView()
        v.backgroundColor = .systemYellow
        return v
    }()
    
    let contentView: UIView = {
        let v = UIView()
        v.backgroundColor = .systemBlue
        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
    }()
    
    // 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."
    
    // update the center-label text when numberOfParagraphs changes
    var numberOfParagraphs = 1 {
        didSet {
            var s = ""
            for i in 1...numberOfParagraphs {
                s += "\(i). " + centerSampleText
                if i < numberOfParagraphs {
                    s += "\n\n"
                }
            }
            centerLabel.text = s
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let btnA = UIButton()
        btnA.setTitle("Add", for: [])
        btnA.setTitleColor(.white, for: .normal)
        btnA.setTitleColor(.lightGray, for: .highlighted)
        btnA.backgroundColor = .systemGreen

        let btnB = UIButton()
        btnB.setTitle("Remove", for: [])
        btnB.setTitleColor(.white, for: .normal)
        btnB.setTitleColor(.lightGray, for: .highlighted)
        btnB.backgroundColor = .systemRed

        [btnA, btnB, theScrollView, contentView, stackView, topLabel, centerLabel, bottomLabel].forEach {
            $0.translatesAutoresizingMaskIntoConstraints = false
        }
        
        view.addSubview(btnA)
        view.addSubview(btnB)
        view.addSubview(theScrollView)
        theScrollView.addSubview(contentView)
        contentView.addSubview(stackView)
        stackView.addArrangedSubview(topLabel)
        stackView.addArrangedSubview(centerLabel)
        stackView.addArrangedSubview(bottomLabel)
        
        let g = view.safeAreaLayoutGuide
        let cg = theScrollView.contentLayoutGuide
        let fg = theScrollView.frameLayoutGuide
        
        // constrain height of content view to height of scroll view's Frame Layout Guide
        //  with less-than-required Priority so it can get taller when the content gets taller
        let contentViewHeightConstraint = contentView.heightAnchor.constraint(equalTo: fg.heightAnchor, constant: 0.0)
        contentViewHeightConstraint.priority = .defaultLow
        
        NSLayoutConstraint.activate([
            
            // constrain buttons at top
            btnA.topAnchor.constraint(equalTo: g.topAnchor, constant: 8.0),
            btnA.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),

            btnB.topAnchor.constraint(equalTo: g.topAnchor, constant: 8.0),
            btnB.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            
            btnB.leadingAnchor.constraint(equalTo: btnA.trailingAnchor, constant: 20.0),
            btnB.widthAnchor.constraint(equalTo: btnA.widthAnchor),

            // constrain scroll view Top to buttons Bottom plus 8-points "spacing"
            //  leading/trailing/bottom to the safe area
            theScrollView.topAnchor.constraint(equalTo: btnA.bottomAnchor, constant: 8.0),
            theScrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
            theScrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
            theScrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),

            // constrain all 4 sides of the content view to the scroll view's Content Layout Guide
            contentView.topAnchor.constraint(equalTo: cg.topAnchor, constant: 0.0),
            contentView.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 0.0),
            contentView.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: 0.0),
            contentView.bottomAnchor.constraint(equalTo: cg.bottomAnchor, constant: 0.0),

            // constrain width of content view to width of scroll view's Frame Layout Guide
            contentView.widthAnchor.constraint(equalTo: fg.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"
        
        numberOfParagraphs = 1
        
        btnA.addTarget(self, action: #selector(addTapped(_:)), for: .touchUpInside)
        btnB.addTarget(self, action: #selector(removeTapped(_:)), for: .touchUpInside)
        
    }
    
    @objc func addTapped(_ sender: Any?) {
        numberOfParagraphs += 1
    }
    @objc func removeTapped(_ sender: Any?) {
        if numberOfParagraphs > 1 {
            numberOfParagraphs -= 1
        }
    }

}

With one or two paragraphs, the content is not tall enough to scroll, but remains vertically centered:

enter image description here enter image description here

With 3 or more paragraphs (on an iPhone SE), we can now scroll:

enter image description here enter image description here

Elevon answered 8/6, 2018 at 19:49 Comment(16)
Thank you very much for your very detailed answer, I'll take some time to properly understand this and then implement it. Stack's awesome because of people like you :D.Vagabond
I edited my answer... forgot to clarify: set stackView to Vertical / Fill / Fill / Spacing: 20Elevon
This worked. I thought about using StackViews but it never hit me I could set the vertical alignment and use >=. Thanks again.Vagabond
Thank you for a clear explanation, searched for hours how to get this done!Clipping
Thanks for the answer and an explanationIsabea
Has anyone accomplished this with programmatic constraints? Storyboards are a poor choice.Ineducation
@MatthewReilly --- very simple to do via code - possibly even easier than in IBElevon
@MatthewReilly - I just added a code-only version (see edits in my answer)Elevon
Amazing. Much appreciated.Ineducation
Wow! You made my day! I spent a whole day to figure this out. What a great answer!. I appreciate for your time and effort on this. Thank you so much! @ElevonDiamonddiamondback
I have one question. Why should I set "change contentView height constraint to Priority: 250" this?Diamonddiamondback
@Diamonddiamondback - by setting a low priority on the height constraint, we're telling auto-layout "you can ignore this constraint if you need to." So, when our content is short, auto-layout maintains the height. When our content gets taller, auto-layout ignores that constraint and allows the height to expand.Elevon
@Elevon I see. Now I can more clearly understand. Thank you so much for the kind explanation about this! DonMag! 😄👍Diamonddiamondback
I've followed this solution but it doesn't align in center on iPhone 5S, here is my question #61830358 Am I doing anything wrong?Anticlinorium
Wow! You get the gold star. 👍Zephyrus
Amazing, what an afford. Really appreciated.Anaphylaxis
I
6

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):

  1. 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.
  2. 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.
  3. 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.
  4. (optional) 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!

Ignite answered 11/8, 2020 at 0:49 Comment(4)
Thanks @Schemetrical, this is a nice and clean approach.Vagabond
This is great @Schemetrical, thanks!Fab
How do you set (1) in IB? It is not possible to do so.Gramineous
@Gramineous I'm not sure if it's possible, I personally don't use IB.Ignite
F
2

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:

  1. Top, bottom, leading, trailing child view constrained to the scrollView content layout guide
  2. Child view width equal to the scrollView width

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.

Fishbowl answered 8/9, 2020 at 13:44 Comment(0)
L
2

Huge props to DonMag for the great explanation. For some reason however, when I implemented it, my ScrollView would not scroll. Here's what worked for me, in Xcode 13's Interface Builder, so you can see the ScrollView's Content Layout Guide vs Frame Layout Guide constraints.

enter image description here

Steps:

  • Drag a Scroll View into your view controller. Pin leading/top/trailing/bottom to the Safe Area or whatever. This will determine the Scroll View's frame and thus Frame Layout Guide.
  • Drag a dummy regular View into the Scroll View. Pin leading/top/trailing/bottom to the Scroll View's Content Layout Guide (the "inside" of the scroll view). These constraints allow the size of this new View to determine the scroll view's scrollable area: If the View is bigger than the scroll view's frame, then the scroll view will allow scrolling, otherwise it will not. Now we only need to set the size of View; its position is irrelevant. (If Scroll View allows scrolling, it will start "scrolled up" from the top by default.)
  • Our end goal is to center our content within this View. So, the View's minimum size should be the total visible area, which is the Scroll View's frame. Assuming we only want to allow vertical scrolling, set the View's width to the Scroll View's Frame Layout Guide width. Additionally, set the View's height to be greater than or equal to the Scroll View's Frame Layout Guide height, because of course we want to enable vertical scrolling if the content within View is larger than the scroll view's frame.
  • Drag the Stack View (or whatever view/content you want) into the View. Set the Stack View's centerY to its superview's centerY.
  • At this point the content will appear vertically centered, but auto layout does not have enough information yet to determine View's height. We set a minimum height before, but what about a maximum? This can be satisfied by one more constraint: Set the Stack View's height to be less than or equal to Views's height. This will magically ensure that if the Stack View's height grows beyond View's minimum height, View's height will increase accordingly, making the area scrollable. (Note that there are other ways to set View's height, like using top and bottom references to View's subviews, as demonstrated in other answers.)
  • Lastly, set the Stack View's horizontal constraints as needed.

Unfortunately Interface Builder complains that "Scroll View needs constraints for: Y position or height" This might be an Xcode / auto layout bug? I don't see any errors in the console.

Liana answered 21/10, 2021 at 22:10 Comment(1)
the most explanatory one. works like a charm. not even needed to try second time even trying it in horizontal way.Degradation

© 2022 - 2024 — McMap. All rights reserved.