ContentView isn't Showing up Inside ScrollView - ios

I'm having trouble getting my ContentView to show up inside my ScrollView and I don't know what I'm doing wrong to make it not visible. In my screenshot down below, the purple view is my ScrollView which is showing perfectly, and as you will see in my code, my ContentView is red. I tried to change my properties to lazy var to see if that would work, but I'm still having the same issue. Am I doing something wrong in my programmatic UI for my ContentView not to show up, or am I missing something? Thank you!
Screenshot of Problem
FindEmployeeJobRequestController
// MARK: - Properties
lazy var jobInfoCardView: ShadowCardView = {
let view = ShadowCardView()
view.backgroundColor = .white
view.addShadow()
view.setHeight(height: 320)
return view
}()
let scrollView: UIScrollView = {
let sv = UIScrollView()
sv.backgroundColor = .darkPurpleTint
sv.isScrollEnabled = true
return sv
}()
let contentView: UIView = {
let view = UIView()
view.backgroundColor = .red
return view
}()
// MARK: - Helper Functions
fileprivate func configureUI() {
view.addSubview(jobInfoCardView)
jobInfoCardView.anchor(top: circularProgressView.bottomAnchor, left: view.leftAnchor, right: view.rightAnchor,
paddingTop: 52, paddingLeft: 24, paddingRight: 24)
jobInfoCardView.addSubview(scrollView)
scrollView.anchor(top: jobInfoCardView.topAnchor, left: jobInfoCardView.leftAnchor,
bottom: jobInfoCardView.bottomAnchor, right: jobInfoCardView.rightAnchor,
paddingTop: 4, paddingLeft: 4, paddingBottom: 4, paddingRight: 4)
jobInfoCardView.addSubview(contentView)
contentView.anchor(top: scrollView.contentLayoutGuide.topAnchor, left: scrollView.contentLayoutGuide.leftAnchor,
bottom: scrollView.contentLayoutGuide.bottomAnchor, right: scrollView.contentLayoutGuide.rightAnchor)
contentView.anchor(left: scrollView.frameLayoutGuide.leftAnchor, right: scrollView.frameLayoutGuide.rightAnchor)
}
UPDATE
I tried to add my contentView to my scrollView's subview, but it's still not showing. I even tried to add an UIStackView to my contentView to see if that could make it visible, but still not visible.
Updated Screenshot
Updated Code
fileprivate func configureUI() {
scrollView.addSubview(contentView)
contentView.anchor(top: scrollView.contentLayoutGuide.topAnchor, left: scrollView.contentLayoutGuide.leftAnchor,
bottom: scrollView.contentLayoutGuide.bottomAnchor, right: scrollView.contentLayoutGuide.rightAnchor)
contentView.anchor(left: scrollView.frameLayoutGuide.leftAnchor, right: scrollView.frameLayoutGuide.rightAnchor)
waitingOnEmployeeStack.axis = .vertical
waitingOnEmployeeStack.distribution = .fillEqually
waitingOnEmployeeStack.spacing = 4
contentView.addSubview(waitingOnEmployeeStack)
waitingOnEmployeeStack.centerX(inView: contentView)
waitingOnEmployeeStack.anchor(top: contentView.topAnchor, paddingTop: 5)
}

I think you wanted to add the contentView as a subView of the scrollView.
fileprivate func configureUI() {
view.addSubview(jobInfoCardView)
jobInfoCardView.anchor(top: circularProgressView.bottomAnchor, left: view.leftAnchor, right: view.rightAnchor,
paddingTop: 52, paddingLeft: 24, paddingRight: 24)
jobInfoCardView.addSubview(scrollView)
scrollView.anchor(top: jobInfoCardView.topAnchor, left: jobInfoCardView.leftAnchor,
bottom: jobInfoCardView.bottomAnchor, right: jobInfoCardView.rightAnchor,
paddingTop: 4, paddingLeft: 4, paddingBottom: 4, paddingRight: 4)
scrollView.addSubview(contentView)
contentView.anchor(
top: scrollView.contentLayoutGuide.topAnchor,
left: scrollView.contentLayoutGuide.leftAnchor,
bottom: scrollView.contentLayoutGuide.bottomAnchor,
right: scrollView.contentLayoutGuide.rightAnchor
)
contentView.anchor(
left: scrollView.frameLayoutGuide.leftAnchor,
right: scrollView.frameLayoutGuide.rightAnchor
)
}

I think you might want to add contentView as scrollView's subview and not jobInfoCardView's subview

Couple notes...
I strongly recommend using standard constraint syntax - at least while you're learning. We don't know if your .anchor(...) funcs are doing the right thing, and when you review your code it's not entirely clear what might be happening. Once you've really gotten the hang of auto-layout, you may find it easier to use "helper funcs" (personally, I don't).
Also - use comments so both you and we know what your intent is.
Take a look at this...
You haven't shown what ShadowCardView might be, so we'll start with just a plain UIView subclass:
class ShadowCardView: UIView {
}
And an example controller class:
class ViewController: UIViewController {
lazy var jobInfoCardView: ShadowCardView = {
let view = ShadowCardView()
view.backgroundColor = .white
return view
}()
let scrollView: UIScrollView = {
let sv = UIScrollView()
sv.backgroundColor = .purple // .darkPurpleTint
return sv
}()
let contentView: UIView = {
let view = UIView()
view.backgroundColor = .red
return view
}()
let circularProgressView: UIView = {
let view = UIView()
view.backgroundColor = .green
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(red: 0.2, green: 0.6, blue: 0.8, alpha: 1.0)
// we'll be using auto-layout constraints for all views
[jobInfoCardView, scrollView, contentView, circularProgressView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
// add views
view.addSubview(circularProgressView)
view.addSubview(scrollView)
scrollView.addSubview(contentView)
contentView.addSubview(jobInfoCardView)
// respect safe area
let safeG = view.safeAreaLayoutGuide
// to make things a little more readable
let contentG = scrollView.contentLayoutGuide
let frameG = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// let's put the circularProgressView 20-points from the Top
circularProgressView.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 20.0),
// 60-points on each side
circularProgressView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 60.0),
circularProgressView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: -60.0),
// and give it 1:1 ratio
circularProgressView.heightAnchor.constraint(equalTo: circularProgressView.widthAnchor),
// let's put the scrollView 20-points from the Bottom of the progress view
scrollView.topAnchor.constraint(equalTo: circularProgressView.bottomAnchor, constant: 20.0),
// 20-points on each side
scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 20.0),
scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: -20.0),
// and give it 1:1 ratio
scrollView.heightAnchor.constraint(equalTo: scrollView.widthAnchor),
// constrain all 4 sides of contentView to scrollView's Content Layout Guide
// and we'll use 12-points "padding" so we can see its frame inside the scrollView
contentView.topAnchor.constraint(equalTo: contentG.topAnchor, constant: 12.0),
contentView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor, constant: 12.0),
contentView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor, constant: -12.0),
contentView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor, constant: -12.0),
// set the Width of contentView to the Width of scrollView's Frame Layout Guide
// mius 24-points (12 on each side)
contentView.widthAnchor.constraint(equalTo: frameG.widthAnchor, constant: -24.0),
// constrain all 4 sides of jobInfoCardView to contentView
jobInfoCardView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0.0),
jobInfoCardView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 0.0),
jobInfoCardView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 0.0),
jobInfoCardView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0.0),
])
}
}
When run, it will look like this (green view is your circularProgressView and purple view is the scroll view):
That's pretty much what you are already getting. That's because - at the moment - ShadowCardView has no content to control its size. So, it's not even visible.
Let's change ShadowCardView to this:
class ShadowCardView: UIView {
let stackView: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 20
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
stackView.translatesAutoresizingMaskIntoConstraints = false
addSubview(stackView)
NSLayoutConstraint.activate([
// constrain all 4 sides of stackView to self
// and we'll use 8-points "padding" so we can see its frame
stackView.topAnchor.constraint(equalTo: self.topAnchor, constant: 8.0),
stackView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 8.0),
stackView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -8.0),
stackView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -8.0),
])
// give the stackView a border so we can see its frame
stackView.layer.borderColor = UIColor.systemBlue.cgColor
stackView.layer.borderWidth = 1
// add some labels
for i in 1...30 {
let v = UILabel()
v.text = "Label \(i)"
v.backgroundColor = .cyan
stackView.addArrangedSubview(v)
}
}
}
We've added a vertical stack view with 30 labels (along with proper constraints), and the output is now:
and we can scroll:

Related

ios Swift: ScrollView with dynamic content programmatic layout

Need to create custom view, just 2 buttons and some content between. Problem is about create correct layout using scrollView and subviews with dynamic content.
For example, if there will be only one Label.
What is my mistake?
Now label isn't visible, and view looks like:
Here is code:
view inits this way:
let view = MyView(frame: .zero)
view.configure(with ...) //here configures label text
selv.view.addSubView(view)
public final class MyView: UIView {
private(set) var titleLabel: UILabel?
override public init(frame: CGRect) {
let closeButton = UIButton(type: .system)
closeButton.translatesAutoresizingMaskIntoConstraints = false
(button setup)
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.showsVerticalScrollIndicator = false
scrollView.alwaysBounceVertical = false
let contentLayoutGuide = scrollView.contentLayoutGuide
let titleLabel = UILabel()
titleLabel.translatesAutoresizingMaskIntoConstraints = false
(label's font and alignment setup)
let successButton = UIButton(type: .system)
successButton.translatesAutoresizingMaskIntoConstraints = false
(button setup)
super.init(frame: frame)
addSubview(closeButton)
addSubview(scrollView)
addSubview(successButton)
scrollView.addSubview(titleLabel)
self.textLabel = textLabel
let layoutGuide = UILayoutGuide()
addLayoutGuide(layoutGuide)
NSLayoutConstraint.activate([
layoutGuide.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 2),
trailingAnchor.constraint(equalToSystemSpacingAfter: layoutGuide.trailingAnchor, multiplier: 2),
layoutGuide.topAnchor.constraint(equalToSystemSpacingBelow: topAnchor, multiplier: 2),
bottomAnchor.constraint(equalToSystemSpacingBelow: layoutGuide.bottomAnchor, multiplier: 2),
closeButton.leadingAnchor.constraint(greaterThanOrEqualTo: layoutGuide.leadingAnchor),
layoutGuide.trailingAnchor.constraint(greaterThanOrEqualTo: closeButton.trailingAnchor),
closeButton.centerXAnchor.constraint(equalTo: layoutGuide.centerXAnchor),
closeButton.topAnchor.constraint(equalTo: layoutGuide.topAnchor),
closeButton.heightAnchor.constraint(equalToConstant: 33),
scrollView.topAnchor.constraint(equalTo: closeButton.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: successButton.topAnchor),
scrollView.contentLayoutGuide.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
scrollView.contentLayoutGuide.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),
successButton.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
layoutGuide.trailingAnchor.constraint(equalTo: successButton.trailingAnchor),
successButton.heightAnchor.constraint(equalToConstant: 48),
layoutGuide.bottomAnchor.constraint(equalTo: successButton.bottomAnchor),
titleLabel.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 16),
titleLabel.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 16),
titleLabel.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -16),
titleLabel.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -16),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func configure(with viewModel: someViewModel) {
titleLabel?.text = viewModel.title
}
}
If I'll add scrollView frameLayoutGuide height:
scrollView.frameLayoutGuide.heightAnchor.constraint(equalToConstant: 150),
, then all looks as expected, but I need to resize this label and all MyView height depending on content.
A UIScrollView is designed to automatically allow scrolling when its content is larger than its frame.
By itself, a scroll view has NO intrinsic size. It doesn't matter how many subviews you add to it... if you don't do something to set its frame, its frame size will always be .zero.
If we want to get the scroll view's frame to grow in height based on its content we need to give it a height constraint when the content size changes.
If we want it to scroll when it has a lot of content, we also need to give it a maximum height.
So, if we want MyView height to be max of 1/2 the screen (view) height, we constrain its height (in the controller) like this:
myView.heightAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.heightAnchor, multiplier: 0.5)
and then constrain the scroll view height in MyView like this:
let svh = scrollView.heightAnchor.constraint(equalToConstant: scrollView.contentSize.height)
svh.priority = .required - 1
svh.isActive = true
Here is a modification to your code - lots of comments in the code so you should be able to follow.
First, an example controller:
class MVTestVC: UIViewController {
let myView = MyView()
let sampleStrings: [String] = [
"Short string.",
"This is a longer string which should wrap onto a couple lines.",
"Now let's use a really, really long string. This will make the label taller, but still not enough to require vertical scrolling.",
"We want to see what happens when we DO need scrolling.\n\nSo, let's use a long string, with some embedded newlines.\n\nThis will make the label tall enough that it would exceed one-half the screen height, so we can see that we do, in fact, get vertical scrolling.",
]
var strIndex: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .gray
myView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(myView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// 20-points on each side
myView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
myView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// centered vertically
myView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
// max 1/2 screen (view) height
myView.heightAnchor.constraint(lessThanOrEqualTo: g.heightAnchor, multiplier: 0.5),
])
myView.backgroundColor = .white
myView.configure(with: sampleStrings[0])
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
strIndex += 1
myView.configure(with: sampleStrings[strIndex % sampleStrings.count])
}
}
and the modified MyView class:
public final class MyView: UIView {
private let titleLabel = UILabel()
private let scrollView = UIScrollView()
// this will be used to set the scroll view height
private var svh: NSLayoutConstraint!
override public init(frame: CGRect) {
super.init(frame: frame)
let closeButton = UIButton(type: .system)
closeButton.translatesAutoresizingMaskIntoConstraints = false
//(button setup)
closeButton.setTitle("X", for: [])
closeButton.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.showsVerticalScrollIndicator = false
scrollView.alwaysBounceVertical = false
titleLabel.translatesAutoresizingMaskIntoConstraints = false
//(label's font and alignment setup)
titleLabel.font = .systemFont(ofSize: 24.0, weight: .light)
titleLabel.numberOfLines = 0
let successButton = UIButton(type: .system)
successButton.translatesAutoresizingMaskIntoConstraints = false
//(button setup)
successButton.setTitle("Success", for: [])
successButton.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
addSubview(closeButton)
addSubview(scrollView)
addSubview(successButton)
scrollView.addSubview(titleLabel)
let layoutGuide = UILayoutGuide()
addLayoutGuide(layoutGuide)
let contentLayoutGuide = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
layoutGuide.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 2),
trailingAnchor.constraint(equalToSystemSpacingAfter: layoutGuide.trailingAnchor, multiplier: 2),
layoutGuide.topAnchor.constraint(equalToSystemSpacingBelow: topAnchor, multiplier: 2),
bottomAnchor.constraint(equalToSystemSpacingBelow: layoutGuide.bottomAnchor, multiplier: 2),
closeButton.leadingAnchor.constraint(greaterThanOrEqualTo: layoutGuide.leadingAnchor),
layoutGuide.trailingAnchor.constraint(greaterThanOrEqualTo: closeButton.trailingAnchor),
closeButton.centerXAnchor.constraint(equalTo: layoutGuide.centerXAnchor),
closeButton.topAnchor.constraint(equalTo: layoutGuide.topAnchor),
closeButton.heightAnchor.constraint(equalToConstant: 33),
scrollView.topAnchor.constraint(equalTo: closeButton.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: successButton.topAnchor),
successButton.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
layoutGuide.trailingAnchor.constraint(equalTo: successButton.trailingAnchor),
successButton.heightAnchor.constraint(equalToConstant: 48),
layoutGuide.bottomAnchor.constraint(equalTo: successButton.bottomAnchor),
// constrain the label to the scroll view's Content Layout Guide
titleLabel.topAnchor.constraint(equalTo: contentLayoutGuide.topAnchor, constant: 16),
titleLabel.leadingAnchor.constraint(equalTo: contentLayoutGuide.leadingAnchor, constant: 16),
titleLabel.trailingAnchor.constraint(equalTo: contentLayoutGuide.trailingAnchor, constant: -16),
titleLabel.bottomAnchor.constraint(equalTo: contentLayoutGuide.bottomAnchor, constant: -16),
// label needs a width anchor, otherwise we'll get horizontal scrolling
titleLabel.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -32),
])
layer.cornerRadius = 12
// so we can see the framing
scrollView.backgroundColor = .red
titleLabel.backgroundColor = .green
}
public override func layoutSubviews() {
super.layoutSubviews()
// we want to update the scroll view's height constraint when the text changes
if let c = svh {
c.isActive = false
}
// on initial layout, the scroll view's content size will still be zero
// so force another layout pass
if scrollView.contentSize.height == 0 {
scrollView.setNeedsLayout()
scrollView.layoutIfNeeded()
}
// constrain the scroll view's height to the height of its content
// but with a less-than-required priority so we can use a maximum height
svh = scrollView.heightAnchor.constraint(equalToConstant: scrollView.contentSize.height)
svh.priority = .required - 1
svh.isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//public func configure(with viewModel: someViewModel) {
// titleLabel.text = viewModel.title
//}
public func configure(with str: String) {
titleLabel.text = str
// force the scroll view to update its layout
scrollView.setNeedsLayout()
scrollView.layoutIfNeeded()
// force self to update its layout
self.setNeedsLayout()
self.layoutIfNeeded()
}
}
Each tap anywhere on the screen will cycle through a few sample strings to change the text in the label, giving us this:

iOS: How to scroll navigation bar as view controller scrolls

I want to scroll the navigation bar as the user scrolls on the view controller. This should be similar to how the YouTube app's home page is working. When the user scrolls down, the navigation bar should be made visible. The navigation bar should move as much as the scroll amount.
I'm aware of hidesBarOnSwipe and setNavigationBarHidden, but these do not give precise control of the y-axis. I'm also reading that Apple does not support directly modifying the navigation bar frame.
So, how does YouTube do this? I'm looking for an MVP demonstrating navigation bar position change along with a UIScrollView offset change.
Without additional detail about what you want to do, I'll make some guesses.
First, the top of the YouTube app's home page is almost certainly not a UINavigationBar -- it doesn't behave like one, there is no pushing/popping of controllers going on, it's in a tab bar controller setup, etc.
So, let's assume it's a view with subviews - we'll call it a "sliding header view" - and your goal is:
don't let the header view's top scroll down
"push it up" when scrolling up
"pull it down" when scrolling down
We can accomplish this by constraining the Top of that header view to the Top of the scroll view's Frame Layout Guide.
when we start to scroll, we'll save the current .contentOffset.y
when we scroll, we'll get the relative scroll y distance
if we're scrolling Up, we'll change the Top Constraint .constant value to move the header view up
if we're scrolling Down, we'll change the Top Constraint .constant value to move the header view down
Here's how it will look at the start:
as we scroll up just a little:
after we've scrolled up farther:
as we scroll down just a little:
after we've scrolled down farther:
Here's the example code for that:
Simple two-label "header" view
class SlidingHeaderView: UIView {
// simple view with two labels
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
backgroundColor = .systemBlue
let v1 = UILabel()
v1.translatesAutoresizingMaskIntoConstraints = false
v1.text = "Label 1"
v1.backgroundColor = .yellow
addSubview(v1)
let v2 = UILabel()
v2.translatesAutoresizingMaskIntoConstraints = false
v2.text = "Label 2"
v2.backgroundColor = .yellow
addSubview(v2)
NSLayoutConstraint.activate([
v1.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12.0),
v1.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12.0),
v2.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12.0),
v2.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12.0),
v1.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
v2.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),
v2.topAnchor.constraint(equalTo: v1.bottomAnchor, constant: 4.0),
v2.heightAnchor.constraint(equalTo: v1.heightAnchor),
])
}
}
example view controller
class SlidingHeaderViewController: UIViewController {
let scrollView: UIScrollView = {
let v = UIScrollView()
v.contentInsetAdjustmentBehavior = .never
return v
}()
let slidingHeaderView: SlidingHeaderView = {
let v = SlidingHeaderView()
return v
}()
let contentView: UIView = {
let v = UIView()
v.backgroundColor = .systemYellow
return v
}()
// Top constraint for the slidingHeaderView
var slidingViewTopC: NSLayoutConstraint!
// to track the scroll activity
var curScrollY: CGFloat = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
[scrollView, slidingHeaderView, contentView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
// add contentView and slidingHeaderView to the scroll view
[contentView, slidingHeaderView].forEach { v in
scrollView.addSubview(v)
}
// add scroll view to self.view
view.addSubview(scrollView)
let safeG = view.safeAreaLayoutGuide
let contentG = scrollView.contentLayoutGuide
let frameG = scrollView.frameLayoutGuide
// we're going to change slidingHeaderView's Top constraint relative to the Top of the scroll view FRAME
slidingViewTopC = slidingHeaderView.topAnchor.constraint(equalTo: frameG.topAnchor, constant: 0.0)
NSLayoutConstraint.activate([
// scroll view Top to view Top
scrollView.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 0.0),
// scroll view Leading/Trailing/Bottom to safe area
scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 0.0),
scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: 0.0),
scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: 0.0),
// constrain slidingHeaderView Top to scroll view's FRAME
slidingViewTopC,
// slidingHeaderView to Leading/Trailing of scroll view FRAME
slidingHeaderView.leadingAnchor.constraint(equalTo: frameG.leadingAnchor, constant: 0.0),
slidingHeaderView.trailingAnchor.constraint(equalTo: frameG.trailingAnchor, constant: 0.0),
// no Height or Bottom constraint for slidingHeaderView
// content view Top/Leading/Trailing/Bottom to scroll view's CONTENT GUIDE
contentView.topAnchor.constraint(equalTo: contentG.topAnchor, constant: 0.0),
contentView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor, constant: 0.0),
contentView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor, constant: 0.0),
contentView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor, constant: 0.0),
// content view Width to scroll view's FRAME
contentView.widthAnchor.constraint(equalTo: frameG.widthAnchor, constant: 0.0),
])
// add some content to the content view so we have something to scroll
addSomeContent()
// because we're going to track the scroll offset
scrollView.delegate = self
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if slidingHeaderView.frame.height == 0 {
// get the size of the slidingHeaderView
let sz = slidingHeaderView.systemLayoutSizeFitting(CGSize(width: scrollView.frame.width, height: .greatestFiniteMagnitude), withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow)
// use its Height for the scroll view's Top contentInset
scrollView.contentInset = UIEdgeInsets(top: sz.height, left: 0, bottom: 0, right: 0)
}
}
func addSomeContent() {
// create a vertical stack view with a bunch of labels
// and add it to our content view so we have something to scroll
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 32
stack.backgroundColor = .gray
stack.translatesAutoresizingMaskIntoConstraints = false
for i in 1...20 {
let v = UILabel()
v.text = "Label \(i)"
v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
v.heightAnchor.constraint(equalToConstant: 48.0).isActive = true
stack.addArrangedSubview(v)
}
contentView.addSubview(stack)
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16.0),
stack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16.0),
stack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16.0),
stack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16.0),
])
}
}
extension SlidingHeaderViewController: UIScrollViewDelegate {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
curScrollY = scrollView.contentOffset.y
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let diffY = scrollView.contentOffset.y - curScrollY
var newY: CGFloat = slidingViewTopC.constant - diffY
if diffY < 0 {
// we're scrolling DOWN
newY = min(newY, 0.0)
} else {
// we're scrolling UP
if scrollView.contentOffset.y <= -slidingHeaderView.frame.height {
newY = 0.0
} else {
newY = max(-slidingHeaderView.frame.height, newY)
}
}
// update slidingHeaderView Top constraint constant
slidingViewTopC.constant = newY
curScrollY = scrollView.contentOffset.y
}
}
Everything is done via code - no #IBOutlet or #IBAction connections needed.

Horizontal scrolling collection view partially hiding first cell content on initial load

I have a UIView and inside that, I have added a UILabel and UICollectionView. The UICollectionView scrolls horizontally. But the issue is that when I run the app, the UICollectionView offset is set in such a way that the first cell is partly hidden. If I scroll to the right then I can see the contents of the first cell. But when I let go it bounces back to its original hidden state.
As you can see here the first cell has 2 UIViews but only the second one is visible.
When I scroll right, this is how it looks:
The, I... 10, is the cell that is getting hidden. I have added the elements programmatically, so could be a constraint issue. But I can't zero down on what could be causing it.
Here's my UICollectionViewCell:
class UserCountryCell: UICollectionViewCell {
let countryNameLabel: UILabel
let countryUserCountLabel: UILabel
override init(frame: CGRect) {
countryNameLabel = UILabel()
countryUserCountLabel = UILabel()
super.init(frame: frame)
self.addSubview(countryNameLabel)
countryNameLabel.translatesAutoresizingMaskIntoConstraints = false
countryNameLabel.font = UIFont.boldSystemFont(ofSize: 13)
countryNameLabel.textColor = .white
self.addSubview(countryUserCountLabel)
countryUserCountLabel.translatesAutoresizingMaskIntoConstraints = false
countryUserCountLabel.font = UIFont.systemFont(ofSize: 13)
countryUserCountLabel.textColor = .white
NSLayoutConstraint.activate([
countryNameLabel.trailingAnchor.constraint(equalTo: self.leadingAnchor, constant: 5),
countryNameLabel.heightAnchor.constraint(equalToConstant: 30),
countryNameLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 5),
countryNameLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 5),
// countryNameLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 0),
countryNameLabel.widthAnchor.constraint(equalToConstant: 20),
countryUserCountLabel.leadingAnchor.constraint(equalTo: countryNameLabel.trailingAnchor, constant: 5),
countryUserCountLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 5),
countryUserCountLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 5),
countryUserCountLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -5),
countryUserCountLabel.heightAnchor.constraint(equalToConstant: 30),
countryUserCountLabel.widthAnchor.constraint(equalToConstant: 40)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureCell(countryName: String, countryUserCount: Int) {
countryNameLabel.text = countryName
countryUserCountLabel.text = String(countryUserCount)
}
}
OK - couple things...
As I said in my comment, add subviews and constraints to the cell's .contentView, not to the cell itself.
You do have a few mistakes in your constraints. You constrained the countryNameLabel.trailingAnchor to self.leadingAnchor ... that should be .leadingAnchor to .leadingAnchor.
Your .bottomAnchor constants should be negative.
If you want the labels' text to determine their widths, don't assign a .widthAnchor.
Try replacing your init with this:
override init(frame: CGRect) {
countryNameLabel = UILabel()
countryUserCountLabel = UILabel()
super.init(frame: frame)
contentView.addSubview(countryNameLabel)
countryNameLabel.translatesAutoresizingMaskIntoConstraints = false
countryNameLabel.font = UIFont.boldSystemFont(ofSize: 13)
countryNameLabel.textColor = .white
contentView.addSubview(countryUserCountLabel)
countryUserCountLabel.translatesAutoresizingMaskIntoConstraints = false
countryUserCountLabel.font = UIFont.systemFont(ofSize: 13)
countryUserCountLabel.textColor = .white
NSLayoutConstraint.activate([
// needs to be .leadingAnchor to .leadingAnchor
//countryNameLabel.trailingAnchor.constraint(equalTo: self.leadingAnchor, constant: 5),
countryNameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 5),
countryNameLabel.heightAnchor.constraint(equalToConstant: 30),
countryNameLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5),
countryNameLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5),
// if you want the label width to fit its text
// don't set the label's widthAnchor
//countryNameLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 0),
//countryNameLabel.widthAnchor.constraint(equalToConstant: 20),
countryUserCountLabel.leadingAnchor.constraint(equalTo: countryNameLabel.trailingAnchor, constant: 5),
countryUserCountLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5),
countryUserCountLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5),
countryUserCountLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -5),
countryUserCountLabel.heightAnchor.constraint(equalToConstant: 30),
// if you want the label width to fit its text
// don't set the label's widthAnchor
//countryUserCountLabel.widthAnchor.constraint(equalToConstant: 40)
])
}

How can a view determine its height based on a function of its width without using an intrinsicContentSize that is dependent on the frame?

I am trying to figure out how best to create a view that determines its own height based on the width it is given. The behaviour I desire is very similar to how a vertical UIStackView behaves in that:
When constrained by its top, leading, and trailing edges, it should determine its own natural height based on its content.
When constrained on all edges, it should fill all the available space by expanding and collapsing as determined by those constraints.
In addition, I am looking to achieve this without using autolayout internally to my view.
To illustrate this, please consider the following example:
class ViewController: UIViewController {
let v = View()
override func viewDidLoad() {
super.viewDidLoad()
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
NSLayoutConstraint.activate([
v.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
v.leadingAnchor.constraint(equalTo: view.leadingAnchor),
v.trailingAnchor.constraint(equalTo: view.trailingAnchor),
// Toggle this constraint on and off.
// v.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
])
}
}
class View: UIView {
// Imagine this came from computing the size of some child views
// and that it is relative to the width of the bounds.
var contentHeight: CGFloat { bounds.width * 0.5 }
var boundsWidth: CGFloat = 0 {
didSet { if oldValue != boundsWidth { invalidateIntrinsicContentSize() } }
}
override var intrinsicContentSize: CGSize {
.init(width: bounds.width, height: contentHeight)
}
override func layoutSubviews() {
super.layoutSubviews()
boundsWidth = bounds.width
backgroundColor = .red
}
}
This example achieves the behaviour I've described above but I have a big reservation with it because of how I'm using intrinsicContentSize.
The documentation for instrinsicContentSize states that it must be independent of the content frame, which I have not managed. I am calculating the intrinsic size based on the width of the frame.
How is it possible to achieve this behaviour and not make instrinsicContentSize rely on the bounds?
This can all be done with auto-layout constraints. No need to calculate anything.
All you need to do is make sure your custom view's content has appropriate constraints to define the layout.
For example, a UILabel has an intrinsic size based on its text. You can constrain a "top" label to the top of the view, a "middle" label to the bottom of the "top" label, and a "bottom" label to the bottom of the "middle" label and to the bottom of the view.
Here's an example (all via code):
class SizeTestViewController: UIViewController {
let v = ExampleView()
override func viewDidLoad() {
super.viewDidLoad()
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
NSLayoutConstraint.activate([
v.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
v.leadingAnchor.constraint(equalTo: view.leadingAnchor),
v.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])
v.topLabel.text = "This is the top label."
v.middleLabel.text = "This is a bunch of text for the middle label. Since we have it set to numberOfLines = 0, the text will wrap onto mutliple lines (assuming it needs to)."
v.bottomLabel.text = "This is the bottom label text\nwith embedded newline characters\nso we can see the multiline feature without needing word wrap."
// so we can see the view's frame
v.backgroundColor = .red
}
}
class ExampleView: UIView {
var topLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .yellow
v.numberOfLines = 0
return v
}()
var middleLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .cyan
v.numberOfLines = 0
return v
}()
var bottomLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .green
v.numberOfLines = 0
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
addSubview(topLabel)
addSubview(middleLabel)
addSubview(bottomLabel)
NSLayoutConstraint.activate([
// constrain topLabel 8-pts from top, leading, trailing
topLabel.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
topLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
topLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
// constrain middleLabel 8-pts from topLabel
// 8-pts from leading, trailing
middleLabel.topAnchor.constraint(equalTo: topLabel.bottomAnchor, constant: 8.0),
middleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
middleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
// constrain bottomLabel 8-pts from middleLabel
// 8-pts from leading, trailing
// 8-pts from bottom
bottomLabel.topAnchor.constraint(equalTo: middleLabel.bottomAnchor, constant: 8.0),
bottomLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
bottomLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
bottomLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),
])
}
}
The result:
and rotated, so you can see the auto-sizing:
Edit
A little clarification on Intrinsic Content Size...
In this image, all 5 subviews have an intrinsicContentSize of 120 x 80:
class IntrinsicTestView: UIView {
override var intrinsicContentSize: CGSize {
return CGSize(width: 120, height: 80)
}
}
As you can see:
If I don't add constraints to specify Width - either with a Width constraint or Leading and Trailing constraints - the view will be 120-pts wide.
If I don't add constraints to specify Height - either with a Height constraint or Top and Bottom constraints - the view will be 80-pts tall.
Otherwise, the width and height will be determined by the constraints I've added.
Here's the complete code for that example:
class IntrinsicTestView: UIView {
override var intrinsicContentSize: CGSize {
return CGSize(width: 120, height: 80)
}
}
class IntrinsicExampleViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var iViews: [IntrinsicTestView] = [IntrinsicTestView]()
var v: IntrinsicTestView
let colors: [UIColor] = [.red, .green, .blue, .yellow, .purple]
colors.forEach { c in
let v = IntrinsicTestView()
v.backgroundColor = c
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
iViews.append(v)
}
let g = view.safeAreaLayoutGuide
// first view at Top: 20 / Leading: 20
v = iViews[0]
v.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0).isActive = true
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0).isActive = true
// second view at Top: 120 / Leading: 20
// height: 20
v = iViews[1]
v.topAnchor.constraint(equalTo: g.topAnchor, constant: 120.0).isActive = true
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0).isActive = true
v.heightAnchor.constraint(equalToConstant: 20).isActive = true
// third view at Top: 160 / Leading: 20
// height: 40 / width: 250
v = iViews[2]
v.topAnchor.constraint(equalTo: g.topAnchor, constant: 160.0).isActive = true
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0).isActive = true
v.heightAnchor.constraint(equalToConstant: 40).isActive = true
v.widthAnchor.constraint(equalToConstant: 250).isActive = true
// fourth view at Top: 220
// trailing: 20 / width: 250
v = iViews[3]
v.topAnchor.constraint(equalTo: g.topAnchor, constant: 220.0).isActive = true
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0).isActive = true
v.widthAnchor.constraint(equalToConstant: 250).isActive = true
// fourth view at Top: 400 / Leading: 20
// trailing: 20 / bottom: 20
v = iViews[4]
v.topAnchor.constraint(equalTo: g.topAnchor, constant: 400.0).isActive = true
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0).isActive = true
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0).isActive = true
v.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0).isActive = true
}
}
As DonMag said in his response, intrinsicContentSize is not quite intended for complex layout management but rather as an indication of a View's preferred size when there are not enough constraints for his frame.
However, there is a simple example i can provide to hopefully point you in the right direction.
Let's assume i want a RatioView to be intrinsically sized so that its height is a linear function of its width:
let ratioView = RatioView()
ratioView.ratio = 0.5
in this example, ratioView will have an intrinsic height equal to half of his width.
The code for RatioView is the following:
class RatioView: UIView {
var contentHeight: CGFloat { bounds.width * ratio }
private var kvoObservation: NSKeyValueObservation?
var ratio: CGFloat = 1 {
didSet { invalidateIntrinsicContentSize() }
}
override var intrinsicContentSize: CGSize {
.init(width: bounds.width, height: contentHeight)
}
override func didMoveToSuperview() {
guard let _ = superview else { return }
kvoObservation = observe(\.frame, options: .initial) { [weak self] (_, change) in
self?.invalidateIntrinsicContentSize()
}
}
override func willMove(toSuperview newSuperview: UIView?) {
if newSuperview == nil {
kvoObservation = nil
}
}
}
There are two important highligths in the above code:
the view calls invalidateIntrinsicContentSize to inform the layout system that the value of his intrinsicContentSize property has changed.
the view is constantly monitoring via KVO observation any changes to his frame property, so that it can calculate its new height every time its width changes.
The second step is especially noteworthy: without it, the view wouldn't know when its size is changing and thus wouldn't have any chance to inform the layout system (via invalidateIntrinsicContentSize) that its intrinsicContentSize has been refreshed.

Why can't I see UIStackView in UIScrollView?

I'm going to put UIStackView in UIScrollView. To put it easier, I'm going to make a mind map. The UIStackView I put into UIScrollView is an object of mind map. Thus, The embedded UIStackView will appear in all zones of UIScrollView.
However, although I have specified the location of UIStackView for UIScrollView, UIStackView does not appear in UIScrollView.
My interface builder(Attaches an interface builder image to understand the My View hierarchy.):
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var createObject: UIBarButtonItem!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let redView = UIView()
redView.backgroundColor = .red
let blueView = UIView()
blueView.backgroundColor = .blue
let stackView = UIStackView(arrangedSubviews: [redView, blueView])
stackView.axis = .vertical
stackView.distribution = .fillEqually
stackView.center = scrollView.center
scrollView.addSubview(stackView)
}
}
Neither UIView nor UIStackView have intrinsic sizes.
So you end up adding a stack view of size 0, 0 containing two views, each sized 0, 0, to your scroll view.
You need to apply some sizing somewhere.
This will give you a stack view that is equal width to your scroll view, and twice as tall (so you have a "full-height" blue view and a a "full-height" red view):
class ViewController: UIViewController {
#IBOutlet weak var scrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let redView = UIView()
redView.backgroundColor = .red
let blueView = UIView()
blueView.backgroundColor = .blue
let stackView = UIStackView(arrangedSubviews: [redView, blueView])
stackView.axis = .vertical
stackView.distribution = .fillEqually
scrollView.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 0.0),
stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 0.0),
stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 0.0),
stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: 0.0),
stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: 0.0),
stackView.heightAnchor.constraint(equalTo: scrollView.heightAnchor, multiplier: 2.0),
])
}
}
Edit:
Here is another example. It adds 6 subviews, with specified widths and heights. The stack view properties are changed to:
.alignment = .center
.distribution = .fill
.spacing = 8
The result:
and scrolled down:
and the code:
class ViewController: UIViewController {
#IBOutlet weak var scrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
let colors: [UIColor] = [
.red,
.blue,
.green,
.orange,
.yellow,
.cyan,
]
let sizes: [CGSize] = [
CGSize(width: 100, height: 150),
CGSize(width: 250, height: 100),
CGSize(width: 200, height: 120),
CGSize(width: 160, height: 200),
CGSize(width: 220, height: 180),
CGSize(width: 60, height: 80),
]
let stackView = UIStackView()
stackView.axis = .vertical
stackView.distribution = .fill
stackView.alignment = .center
stackView.spacing = 8
for (color, size) in zip(colors, sizes) {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.widthAnchor.constraint(equalToConstant: size.width).isActive = true
v.heightAnchor.constraint(equalToConstant: size.height).isActive = true
v.backgroundColor = color
stackView.addArrangedSubview(v)
}
scrollView.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 0.0),
stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 0.0),
stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 0.0),
stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: 0.0),
stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: 0.0),
// do NOT set a heightAnchor ... let the subviews define the height
// stackView.heightAnchor.constraint(equalTo: scrollView.heightAnchor, multiplier: 2.0),
])
}
}
You haven't set frame to stackview. Try adding
stackView.frame = UIScreen.main.bounds
after addSubview.

Resources