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