Search code examples
iosswiftuiscrollview

Swift 4 - Programmatically create vertical UIScroll


I have the following Booking class (UIView) that i build up programmatically but i have been unable to get it to scroll. I require that it scrolls vertically only. I am not using UIStackView as the view i am actually using is slightly more complex than the code below. The code provided below is a simplified version.

I have read various SO posts where some say you need to have a view inside the scroll view that then contains all the subviews (i am currenly doing this), other people saying this is not needed. Some say you need to manually define the content size of the scroll view and again, others saying you should not have to do this! What is correct for each of these points? or does it not matter?

When i have updated the UIScrollView to add up all the views heights, i see the vertical scroll indicator move up and down, but the actual content doesnt move!

Surely if the constraints have been set up, the scrollview should be able to calculate the height?

I have also been having issues where the views are stretching vertically and so have been adding in content hugging priority where i setup the constraints.

Here is the code:-

import UIKit

@IBDesignable
class Booking: UIView, UIScrollViewDelegate {

    //Layout Items
    let scrollView: UIScrollView = {
        let view = UIScrollView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.showsVerticalScrollIndicator = true
        view.showsHorizontalScrollIndicator = false
        view.isScrollEnabled = true
        return view
    }()

    let contentView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()

    //Header view and its children
    let headerContentView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = #colorLiteral(red: 0.8078431487, green: 0.02745098062, blue: 0.3333333433, alpha: 1)
        return view
    }()

    let headerBookingLbl: UILabel = {
        let view = UILabel()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.textColor = UIColor.white
        view.numberOfLines = 3
        view.textAlignment = .center
        view.text = "Title Title Title"
        return view
    }()

    let aboutStack: UIStackView = {
        let view = UIStackView()
        view.isUserInteractionEnabled = true
        view.translatesAutoresizingMaskIntoConstraints = false
        view.axis = .horizontal
        view.alignment = .fill
        view.distribution = .fill
        view.spacing = 5
        view.backgroundColor = UIColor.clear
        return view
    }()

    let aboutIcon: UIImageView = {
        let view = UIImageView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.contentMode = .scaleAspectFit
        view.clipsToBounds = true
        view.backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 0)
        return view
    }()

    let aboutLbl: UILabel = {
        let view = UILabel()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.textColor = UIColor.white
        view.text = "About Your Booking"
        view.numberOfLines = 1
        view.textAlignment = .natural
        return view
    }()


    //body view
    let haveABookingLbl: UILabel = {
        let view = UILabel()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.textColor = #colorLiteral(red: 0.8078431487, green: 0.02745098062, blue: 0.3333333433, alpha: 1)
        view.text = "Already have a booking?"
        view.numberOfLines = 0
        view.textAlignment = .center
        return view
    }()

    let enterABookingInfoLbl: UILabel = {
        let view = UILabel()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.textColor = #colorLiteral(red: 0.8078431487, green: 0.02745098062, blue: 0.3333333433, alpha: 1)
        view.text = "Enter your ticket reference number to gain access to bonus content"
        view.numberOfLines = 0
        view.textAlignment = .center
        return view
    }()

    let bookingReference: UITextField = {
        let view = UITextField()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.autocapitalizationType = .none
        view.borderStyle = UITextField.BorderStyle.roundedRect
        view.placeholder = "Ticket reference*"
        return view
    }()

    let lastName: UITextField = {
        let view = UITextField()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.autocapitalizationType = .none
        view.borderStyle = UITextField.BorderStyle.roundedRect
        view.placeholder = "Last name*"
        return view
    }()

    let submitBtn: UIButton = {
        let view = UIButton()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.setTitle("Submit", for: .normal)
        view.backgroundColor = #colorLiteral(red: 0.8078431487, green: 0.02745098062, blue: 0.3333333433, alpha: 1)
        return view
    }()

    let viewPrivacyPolicayLbl: UILabel = {
        let view = UILabel()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.isUserInteractionEnabled = true
        view.textColor = UIColor.black
        view.numberOfLines = 1
        view.textAlignment = .center

        //underline the text - use attributed string
        let text = "view privacy policy"
        let underlinedText = NSMutableAttributedString(string: text)
    underlinedText.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: NSRange(location: 0, length: text.count))
        view.attributedText = underlinedText

        return view
    }()


    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }

    override func awakeFromNib() {
        super.awakeFromNib()
    }

    override func layoutSubviews() {
        super.layoutSubviews()
    }

    //This override should prevent issue where you can drag scrollview 1px left/right which shows a white area behind the poster.
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        scrollView.contentOffset.x = 0
    }

    private func setup() {
        print("setup()")
        setupViews()
        setupConstraints()
        //scrollView.contentSize = contentView.bounds.size    ?? is this needed? doesnt seem to work!
        scrollView.delegate = self
    }

    private func setupViews() {
        print("setupViews()")

        //Add root scroll view
        addSubview(scrollView)
        scrollView.addSubview(contentView)

        //header contents
        contentView.addSubview(headerContentView)
        headerContentView.addSubview(headerBookingLbl)

        headerContentView.addSubview(aboutStack)
        aboutStack.addArrangedSubview(aboutIcon)
        aboutStack.addArrangedSubview(aboutLbl)


        //body contents
        contentView.addSubview(haveABookingLbl)
        contentView.addSubview(enterABookingInfoLbl)
        contentView.addSubview(bookingReference)
        contentView.addSubview(lastName)
        contentView.addSubview(submitBtn)
        contentView.addSubview(viewPrivacyPolicayLbl)

    } //setupViews

    private func setupConstraints() {
        print("setupConstraints()")

        let scrollViewConstraints = [
            scrollView.topAnchor.constraint(equalTo: topAnchor),
            scrollView.bottomAnchor.constraint(equalTo: bottomAnchor),
            scrollView.widthAnchor.constraint(equalTo: widthAnchor),
            scrollView.centerXAnchor.constraint(equalTo: centerXAnchor),

            contentView.centerXAnchor.constraint(equalTo: centerXAnchor),
            contentView.widthAnchor.constraint(equalTo: widthAnchor),
            contentView.topAnchor.constraint(equalTo: topAnchor),
            contentView.bottomAnchor.constraint(equalTo: bottomAnchor),
        ]
        NSLayoutConstraint.activate(scrollViewConstraints)


        //header
        let headerViewConstraints = [
            //container
            headerContentView.topAnchor.constraint(equalTo: topAnchor),
            headerContentView.leadingAnchor.constraint(equalTo: leadingAnchor),
            headerContentView.trailingAnchor.constraint(equalTo: trailingAnchor),

            //headerBookingLbl
            headerBookingLbl.topAnchor.constraint(equalTo: headerContentView.topAnchor, constant: 20),
            headerBookingLbl.leadingAnchor.constraint(equalTo: headerContentView.leadingAnchor, constant: 20),
            headerBookingLbl.trailingAnchor.constraint(equalTo: headerContentView.trailingAnchor, constant: -20),

            //aboutStack
            aboutStack.topAnchor.constraint(equalTo: headerBookingLbl.bottomAnchor, constant: 20),
            aboutStack.centerXAnchor.constraint(equalTo: headerContentView.centerXAnchor),
            aboutStack.bottomAnchor.constraint(equalTo: headerContentView.bottomAnchor, constant: -20)
        ]
        NSLayoutConstraint.activate(headerViewConstraints)

        headerBookingLbl.setContentHuggingPriority(UILayoutPriority(rawValue: 1000), for: .vertical)


        //body
        let bodyViewConstraints = [
            //haveABookingLbl
            haveABookingLbl.topAnchor.constraint(equalTo: headerContentView.bottomAnchor, constant: 30),
            haveABookingLbl.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 60),
            haveABookingLbl.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -60),

            //enterABookingInfoLbl
            enterABookingInfoLbl.centerXAnchor.constraint(equalTo: centerXAnchor),
            enterABookingInfoLbl.topAnchor.constraint(equalTo: haveABookingLbl.bottomAnchor, constant: 15),
            enterABookingInfoLbl.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 60),
            enterABookingInfoLbl.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -60),

            //bookingReference
            bookingReference.heightAnchor.constraint(equalToConstant: 40),
            bookingReference.centerXAnchor.constraint(equalTo: centerXAnchor),
            bookingReference.topAnchor.constraint(equalTo: enterABookingInfoLbl.bottomAnchor, constant: 15),
            bookingReference.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 60),
            bookingReference.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -60),

            //lastName
            lastName.heightAnchor.constraint(equalToConstant: 40),
            lastName.centerXAnchor.constraint(equalTo: centerXAnchor),
            lastName.topAnchor.constraint(equalTo: bookingReference.bottomAnchor, constant: 15),
            lastName.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 60),
            lastName.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -60),

            //submitBtn
            submitBtn.centerXAnchor.constraint(equalTo: centerXAnchor),
            submitBtn.topAnchor.constraint(equalTo: lastName.bottomAnchor, constant: 15),
            submitBtn.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 60),
            submitBtn.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -60),

            viewPrivacyPolicayLbl.centerXAnchor.constraint(equalTo: centerXAnchor),
            viewPrivacyPolicayLbl.topAnchor.constraint(equalTo: submitBtn.bottomAnchor, constant: 15),
            viewPrivacyPolicayLbl.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 60),
            viewPrivacyPolicayLbl.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -60),
            //viewPrivacyPolicayLbl.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20)
        ]
        NSLayoutConstraint.activate(bodyViewConstraints)


        //Set hugging priority to prevent said items from being made larger than their intrinsic size
        haveABookingLbl.setContentHuggingPriority(UILayoutPriority(rawValue: 1000), for: .vertical)
        enterABookingInfoLbl.setContentHuggingPriority(UILayoutPriority(rawValue: 1000), for: .vertical)
        submitBtn.setContentHuggingPriority(UILayoutPriority(rawValue: 1000), for: .vertical)
        viewPrivacyPolicayLbl.setContentHuggingPriority(UILayoutPriority(rawValue: 1000), for: .vertical)

    } //setupConstraints
}

Solution

  • The main reason of adding a content view inside a scroll view is to have auto layout working correctly. The content view has to fill the scroll view and every subviews that is added to the content view needs to setup constraints in the right way using its container as reference:

    private func setupConstraints() {
            print("setupConstraints()")
    
            let scrollViewConstraints = [
                scrollView.topAnchor.constraint(equalTo: topAnchor),
                scrollView.bottomAnchor.constraint(equalTo: bottomAnchor),
                scrollView.widthAnchor.constraint(equalTo: widthAnchor),
                scrollView.centerXAnchor.constraint(equalTo: centerXAnchor),
    
                contentView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
                contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
                contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
                contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor)
            ]
            NSLayoutConstraint.activate(scrollViewConstraints)
    
            // header
            let headerViewConstraints = [
                // container
                headerContentView.topAnchor.constraint(equalTo: contentView.topAnchor),
                headerContentView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
                headerContentView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
    
                // headerBookingLbl
                headerBookingLbl.topAnchor.constraint(equalTo: headerContentView.topAnchor, constant: 20),
                headerBookingLbl.leadingAnchor.constraint(equalTo: headerContentView.leadingAnchor, constant: 20),
                headerBookingLbl.trailingAnchor.constraint(equalTo: headerContentView.trailingAnchor, constant: -20),
    
                // aboutStack
                aboutStack.topAnchor.constraint(equalTo: headerBookingLbl.bottomAnchor, constant: 20),
                aboutStack.centerXAnchor.constraint(equalTo: headerContentView.centerXAnchor),
                aboutStack.bottomAnchor.constraint(equalTo: headerContentView.bottomAnchor, constant: -20)
            ]
            NSLayoutConstraint.activate(headerViewConstraints)
    
            headerBookingLbl.setContentHuggingPriority(UILayoutPriority(rawValue: 1000), for: .vertical)
    
            // body
            let bodyViewConstraints = [
                // haveABookingLbl
                haveABookingLbl.topAnchor.constraint(equalTo: headerContentView.bottomAnchor, constant: 30),
                haveABookingLbl.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 60),
                haveABookingLbl.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -60),
    
                // enterABookingInfoLbl
                enterABookingInfoLbl.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
                enterABookingInfoLbl.topAnchor.constraint(equalTo: haveABookingLbl.bottomAnchor, constant: 15),
                enterABookingInfoLbl.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 60),
                enterABookingInfoLbl.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -60),
    
                // bookingReference
                bookingReference.heightAnchor.constraint(equalToConstant: 40),
                bookingReference.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
                bookingReference.topAnchor.constraint(equalTo: enterABookingInfoLbl.bottomAnchor, constant: 15),
                bookingReference.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 60),
                bookingReference.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -60),
    
                // lastName
                lastName.heightAnchor.constraint(equalToConstant: 40),
                lastName.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
                lastName.topAnchor.constraint(equalTo: bookingReference.bottomAnchor, constant: 15),
                lastName.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 60),
                lastName.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -60),
    
                // submitBtn
                submitBtn.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
                submitBtn.topAnchor.constraint(equalTo: lastName.bottomAnchor, constant: 15),
                submitBtn.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 60),
                submitBtn.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -60),
    
                viewPrivacyPolicayLbl.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
                viewPrivacyPolicayLbl.topAnchor.constraint(equalTo: submitBtn.bottomAnchor, constant: 15),
                viewPrivacyPolicayLbl.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 60),
                viewPrivacyPolicayLbl.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -60),
                viewPrivacyPolicayLbl.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20)
            ]
            NSLayoutConstraint.activate(bodyViewConstraints)
    
    
            // Set hugging priority to prevent said items from being made larger than their intrinsic size
            haveABookingLbl.setContentHuggingPriority(UILayoutPriority(rawValue: 1000), for: .vertical)
            enterABookingInfoLbl.setContentHuggingPriority(UILayoutPriority(rawValue: 1000), for: .vertical)
            submitBtn.setContentHuggingPriority(UILayoutPriority(rawValue: 1000), for: .vertical)
            viewPrivacyPolicayLbl.setContentHuggingPriority(UILayoutPriority(rawValue: 1000), for: .vertical)
    
    } // setupConstraints