viewDidLayoutSubviews() issue - swift

I have the following screen, in order to centre the boxes between the uilabel and the keyboard I created a simple calculation where I added the header height then the uilabel height and then the keyboard height minus the whole screen size and divided everything by two in order to get the centre point [i added the code below although it's not relevant]
The issue is that in order to get the UILabels height I have to set the constraints in viewDidLayoutSubviews(). Now the idea is that every time you fill one box the next box becomes the first responder but that doesn't work because every time a box becomes a first responder viewDidLayoutSubviews() is being called.
What can I do about this ? is there a way to get the height of uiLabel without calling viewDidLayoutSubviews().
This is the code for centering the boxes :
override func viewDidLayoutSubviews() {
let allItems = headerC.frame.height + stackViewOfDefin.frame.height + 270 + 20
let getSpace = UIScreen.main.bounds.height - allItems
let stackview = UIStackView(arrangedSubviews: gameBoxes)
stackview.axis = .horizontal
stackview.spacing = 5
stackview.distribution = .fillEqually
stackview.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(stackview)
NSLayoutConstraint.activate([
stackview.topAnchor.constraint(equalTo: secondLine.bottomAnchor, constant: getSpace/2-30),
stackview.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
stackview.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: 10),
stackview.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -10)
])
}

Related

UIScrollView with vertical stackview scrolls horizontally but not vertically

I followed the exact code of a similar question's answer and while it scrolled vertically for them it does not for me. The label in the code is just a test there are far more in the actual program but they are added from firebase later on so I'm not sure if that changes anything. While it's not super important I would prefer to figure this out programmatically as I am more capable in that area. I'm not great at asking questions or providing the right code so the whole project is here
`
#IBOutlet weak var history: UIStackView!
#IBOutlet weak var scrollView: UIScrollView!
var ref: DatabaseReference!
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(scrollView)
ref = Database.database().reference()
let label = UILabel(frame: CGRect.init())
label.text = "Label"
history.addArrangedSubview(label)
scrollView.contentSize = CGSize(width: view.bounds.width, height: view.bounds.height)
history.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
history.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
history.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
history.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
history.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
//history.heightAnchor.constraint(lessThanOrEqualTo: scrollView.heightAnchor).isActive = true
scrollView.addSubview(history)
view.addSubview(scrollView)
`
You're doing a bunch of things wrong...
Your code shows #IBOutlet for both the history stack view and the scrollView, which implies you've added them in Storyboard? If so, you should not be doing:
scrollView.addSubview(history)
view.addSubview(scrollView)
because they already exist when added in Storyboard. Also, one would expect you to have added the constraints in Storyboard.
However, if you want to do it all from code, flow this extremely simple example:
class ViewController: UIViewController {
var history: UIStackView!
var scrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
history = UIStackView()
// vertical stack
history.axis = .vertical
// arranged subviews fill the width
history.alignment = .fill
// distribution
history.distribution = .fill
// spacing
history.spacing = 12
scrollView = UIScrollView()
// so we can see it
scrollView.backgroundColor = .cyan
// we're using auto-layout constraints
scrollView.translatesAutoresizingMaskIntoConstraints = false
history.translatesAutoresizingMaskIntoConstraints = false
// add the stack view to the scroll view
scrollView.addSubview(history)
// add the scroll view to the view
view.addSubview(scrollView)
// no no no... let auto-layout handle it
//scrollView.contentSize = CGSize(width: view.bounds.width, height: view.bounds.height)
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain scroll view with 20-pts on each side
scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
// constrain stack view to all 4 sides of scroll view with 8-pts on each side
history.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 8.0),
history.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 8.0),
history.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -8.0),
history.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -8.0),
// constrain stack view width to scroll view width minus 16 (for 8-pts on each side)
history.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -16),
])
// let's add 30 labels to the stack view
for i in 1...30 {
let label = UILabel()
label.text = "Label: \(i)"
// so we can see the label frames
label.backgroundColor = .yellow
history.addArrangedSubview(label)
}
}
}
As a side note: it would benefit you greatly to read the documentation and work through several scroll view tutorials. It looks like you tried to use pieces from the question you linked to without knowing what any of it meant.

UIScrollView content inset defined by a height of a view outside of the subtree

Given a view hierarchy:
- View `container`
- UIScrollView `scrollView`
- UIView `content`
- UIView `footer`
I would like the UIScrollView.contentInset.bottom to be equal to footer.bounds.height.
Question: Can this be expressed using Auto Layout?
Now, there is a very evident brute-force approach that I am aware of and that works:
Observe changes to the bounds property of the footer
scrollView.contentInset.bottom = -footer.bounds.height once footer's parent has finished layoutSubviews().
Or alternatively I could have a constraint between content.bottom and scrollView.bottom (which, as, I'm sure, you are aware, indicates the bottom content inset for non-ambiguously size content) and have its constant altered each time the footer bounds change.
But the point is that all of those approaches are very on-the-nose, really makes me uncomfortable for the terrible code they produce so I am wondering:
Can this be expressed using Auto Layout?
I have attempted to do the following:
content.bottomAnchor.constraint(equalTo: footer.topAnchor)
Hoping that content.bottomAnchor would be treated as the bottom inset of the scroll view content, but nope - Auto Layout literally treats it as me constraining content's bottom to the footer's top.
OK - one approach...
As of iOS 11 (I'm assuming you don't need to target earlier than that), a subview of a UIScrollView can be constrained to the scroll view's Frame Layout Guide. This made it easy to add non-scrolling UI elements to the scroll view hierarchy.
Based on this hierarchy:
- view
- scrollView
- contentView
- element1
- element2
- element3
- UILayoutGuide
- footerView
What we'll do is:
add all the "scrollable" elements to the contentView
plus add a UILayoutGuide to the contentView which will serve as or "bottom" scrollable element
add the footerView to the scrollView last so it is at the top of the z-order
constrain the footerView to the scrollView's Frame Layout Guide so it stays put
constrain the heightAnchor of our UILayoutGuide equal to the heightAnchor of the footerView
Because a UILayoutGuide is a non-rendering view, it will not be visible but it will create the space from the bottom of our last viewable element to the bottom of the contentView -- and it will automatically change height if/when the footerView changes height.
Here's a complete example - scrollView / contentView / 3 imageViews / layout guide / translucent footerView:
class ExampleViewController: UIViewController {
let scrollView: UIScrollView = {
let v = UIScrollView()
v.backgroundColor = .lightGray
return v
}()
let contentView: UIView = {
let v = UIView()
v.backgroundColor = .cyan
return v
}()
let footerView: UILabel = {
let v = UILabel()
v.textAlignment = .center
v.textColor = .white
v.font = UIFont.systemFont(ofSize: 24.0, weight: .bold)
v.text = "Footer View"
v.backgroundColor = UIColor.black.withAlphaComponent(0.65)
return v
}()
var imgView1: UIImageView = {
let v = UIImageView()
v.backgroundColor = .red
v.image = UIImage(systemName: "1.circle")
v.tintColor = .white
return v
}()
var imgView2: UIImageView = {
let v = UIImageView()
v.backgroundColor = .green
v.image = UIImage(systemName: "2.circle")
v.tintColor = .white
return v
}()
var imgView3: UIImageView = {
let v = UIImageView()
v.backgroundColor = .blue
v.image = UIImage(systemName: "3.circle")
v.tintColor = .white
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
// add 3 image views as the content we want to see
contentView.addSubview(imgView1)
contentView.addSubview(imgView2)
contentView.addSubview(imgView3)
// add contentView to srollView
scrollView.addSubview(contentView)
// add footer view to scrollView last so it's at the top of the z-order
scrollView.addSubview(footerView)
view.addSubview(scrollView)
[scrollView, contentView, footerView, imgView1, imgView2, imgView3].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
// "spacer" for bottom of scroll content
// we'll constrain it to the height of the footer view
let spacerGuide = UILayoutGuide()
contentView.addLayoutGuide(spacerGuide)
let g = view.safeAreaLayoutGuide
let svCLG = scrollView.contentLayoutGuide
let scFLG = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// constrain scrollView view 40-pts on all 4 sides to view (safe-area)
scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
// contentView view 0-pts top / leading / trailing / bottom to scrollView contentLayoutGuide
contentView.topAnchor.constraint(equalTo: svCLG.topAnchor, constant: 0.0),
contentView.leadingAnchor.constraint(equalTo: svCLG.leadingAnchor, constant: 0.0),
contentView.trailingAnchor.constraint(equalTo: svCLG.trailingAnchor, constant: 0.0),
contentView.bottomAnchor.constraint(equalTo: svCLG.bottomAnchor, constant: 0.0),
// contentView width == scrollView frameLayoutGuide width
contentView.widthAnchor.constraint(equalTo: scFLG.widthAnchor, constant: 0.0),
// imgView1 to top of contentView
imgView1.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0.0),
// imgView1 width / height
imgView1.widthAnchor.constraint(equalToConstant: 240.0),
imgView1.heightAnchor.constraint(equalToConstant: 240.0),
// imgView1 centerX to contentView centerX
imgView1.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
// imgView2 top to bottom of imgView1 + 20-pt spacing
imgView2.topAnchor.constraint(equalTo: imgView1.bottomAnchor, constant: 20.0),
// imgView2 width / height
imgView2.widthAnchor.constraint(equalToConstant: 200.0),
imgView2.heightAnchor.constraint(equalToConstant: 280.0),
// imgView2 centerX to contentView centerX
imgView2.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
// imgView3 top to bottom of imgView2 + 20-pt spacing
imgView3.topAnchor.constraint(equalTo: imgView2.bottomAnchor, constant: 20.0),
// imgView3 width / height
imgView3.widthAnchor.constraint(equalToConstant: 280.0),
imgView3.heightAnchor.constraint(equalToConstant: 320.0),
// imgView3 centerX to contentView centerX
imgView3.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
// spacerGuide top to bottom of actual content
// spacerGuide top to imgView3 bottom
spacerGuide.topAnchor.constraint(equalTo: imgView3.bottomAnchor, constant: 0.0),
// spacerGuide to leading / trailing / bottom of contentView
spacerGuide.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 0.0),
spacerGuide.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 0.0),
spacerGuide.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0.0),
// footerView to leading / trailing / bottom of scrollView frameLayoutGuide
// (constrained to frameLayoutGuide so it won't scroll)
footerView.leadingAnchor.constraint(equalTo: scFLG.leadingAnchor, constant: 0.0),
footerView.trailingAnchor.constraint(equalTo: scFLG.trailingAnchor, constant: 0.0),
footerView.bottomAnchor.constraint(equalTo: scFLG.bottomAnchor, constant: 0.0),
// footerView height == scrollView height with 0.25 multiplier
// (so it will change height when scrollView changes height, such as device rotation)
footerView.heightAnchor.constraint(equalTo: scFLG.heightAnchor, multiplier: 0.25),
// finally, spacerGuide height equal to footerView height
spacerGuide.heightAnchor.constraint(equalTo: footerView.heightAnchor),
])
}
}
Result:
Scrolled to the bottom:
and rotated (so we see the footerView height change) scrolled all the way to the bottom:
Edit
The answer to the specific question is: you can't.
A scroll view's contentInset is not an object to which you can add constraints... it's a Property of the scroll view. Much like you could not constrain a scroll view's .backgroundColor to an auto-layout constraint.
Landed here after looking for a solution, in my case the scroll view is actually a UICollectionView, so adding "helper" elements to the layout (as suggested by another answer) would have been more challenging (changing dataSource logic etc)
I ended up doing the following:
(This example assumes you have a bottomView attached to the bottom of the screen and you want your scrollView / collectionView to be inset based on it)
Set scrollView.contentInsetAdjustmentBehavior = .never
Then in your View Controller, do this:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.scrollView.contentInset = .init(top: self.view.safeAreaLayoutGuide.layoutFrame.minY,
left: 0,
bottom: bottomView.frame.height,
right: 0)
}
Note that if you also want to account for left and right safe area (e.g. landscape avoiding iPhone notch), you can do:
[...]
left: self.view.safeAreaLayoutGuide.layoutFrame.minX
[...]
right: self.view.frame.width - self.view.safeAreaLayoutGuide.layoutFrame.maxX

Constraint to set max height for items inside stackView

I have a stackView with UiViews boxes inside as you can see in the picture. I would like to set a max height of 50 or less for the boxes inside as they look too big right now but i also would like to keep the 1:1 ratio so that they would be square. I've tried to set a height constraint to the stackView but when i do that, the aspect ratio of 1:1 is being ignored.
Here's what it looks like right now:
And here's the code:
let stackview = UIStackView(arrangedSubviews: letterBoxes)
stackview.axis = .horizontal
stackview.spacing = 5
stackview.distribution = .fillEqually
stackview.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(stackview)
NSLayoutConstraint.activate([
stackview.topAnchor.constraint(equalTo: secondLine.bottomAnchor, constant: c/2 +
20),
stackview.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
stackview.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 10)
])
for box in letterBoxes{
box.aspectRation(1.0/1.0).isActive = true
}
Edit: i want the box to auto-resize and not overflow like the following (each time there's a different amount of boxes. so for example when there are only 3 boxes i want that the max size will be 50 but when there are 6 or higher number I want it to auto-resize so that it will fit the screen and not overflow. once I take the leading constraint off it overflows) :
The following will do the job.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let letterBoxes = [UIView(), UIView(), UIView()]
for box in letterBoxes {
box.translatesAutoresizingMaskIntoConstraints = false
box.backgroundColor = .systemBlue
view.addSubview(box)
let heightConstraint = box.heightAnchor
.constraint(equalToConstant: 50)
heightConstraint.priority = .defaultHigh
NSLayoutConstraint.activate([
box.widthAnchor.constraint(equalTo: box.heightAnchor),
heightConstraint
])
}
let stackview = UIStackView(arrangedSubviews: letterBoxes)
stackview.axis = .horizontal
stackview.spacing = 5
stackview.distribution = .fillEqually
stackview.alignment = .center
stackview.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackview)
NSLayoutConstraint.activate([
stackview.centerYAnchor.constraint(equalTo: view.centerYAnchor),
stackview.centerXAnchor.constraint(equalTo: view.centerXAnchor),
stackview.leadingAnchor.constraint(
greaterThanOrEqualTo: view.leadingAnchor,
constant: 10
),
stackview.trailingAnchor.constraint(
lessThanOrEqualTo: view.trailingAnchor,
constant: -10
)
])
}
}
The point is to make the priorities of the heighConstraints for the boxes to be less than required.
So when there are not a lot of boxes, they will be of size 50, but when there are many, the height constraints will be ignored.
Also note that the stackview's leadingAnchor and the trailingAnchor constraint keeps the boxes in the screen bounds.
The following screenshots show the result:

UIStackView equal spacing including edges

I have a horizontal stack view with 3 buttons: Backwards, Play, Forward for a music application.
Here is my current code:
self.controlStackView.axis = .horizontal
self.controlStackView.distribution = .equalSpacing
self.controlStackView.alignment = .center
self.controlStackView.spacing = 10.0
self.controlStackView.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(self.controlStackView)
self.controlStackView.topAnchor.constraint(equalTo: self.artworkImageView.bottomAnchor, constant: 10.0).isActive = true
self.controlStackView.centerXAnchor.constraint(equalTo: self.contentView.centerXAnchor).isActive = true
What does this is it distributes the button as follows (from the center due to alignment):
[backward] - 10 spacing - [play] - 10 spacing - [forward]
I could increase the spacing but it would still be fixed.
So I'm setting the leading and trailing anchor to define a maximum width of the stack view:
self.controlStackView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
self.controlStackView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
What this does to the layout:
[left edge backward] - lots of spaaaaaaace - [centered play] - lots of spaaaaaaace - [right edge forward]
It distributes over the entire width (and there is an .equalSpacing between center to the left and right). But this also is not helpful. Essentially my goal is to have true equal spacing including the edges.
Let's say I have an available width of 100 and my 3 buttons are 10, 20, 10 - which means that there is 60 remaining space that is empty.
I would like it to be distributed like this:
space - [backward] - space - [play] - space [forward] - space
So 4 spaces in between my buttons each space being 15, so we fill the 60 remaining space.
I could of course implement padding to the stack view to get the outer space, but this would be quite static and is not equally distributed.
Does anybody know if I can implement it this way that the edges are included into the space distribution?
Thanks
This is really pretty straight-forward, using "spacer" views.
Add one more spacer than the number of buttons, so you have:
spacer - button - spacer - button - spacer
Then, constrain the widths of spacers 2-to-n equal to the width of the first spacer. The stackView will handle the rest!
Here is an example (just needs a viewController in storyboard, the rest is done via code):
class DistributeViewController: UIViewController {
let stackView: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .horizontal
v.alignment = .fill
v.distribution = .fill
v.spacing = 0
return v
}()
var buttonTitles = [
"Backward",
"Play",
"Forward",
// "Next",
]
var numButtons = 0
override func viewDidLoad() {
super.viewDidLoad()
// stackView will hold the buttons and spacers
view.addSubview(stackView)
// constrain it to Top + 20, Leading and Trailing == 0, height will be controlled by button height
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20.0),
stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 0.0),
stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 0.0),
])
// arrays to hold our buttons and spacers
var buttons: [UIView] = [UIView]()
var spacers: [UIView] = [UIView]()
numButtons = buttonTitles.count
// create the buttons and append them to our buttons array
for i in 0..<numButtons {
let b = UIButton()
b.translatesAutoresizingMaskIntoConstraints = false
b.backgroundColor = .blue
b.setTitle(buttonTitles[i], for: .normal)
buttons.append(b)
}
// create the spacer views and append them to our spacers array
// we need 1 more spacer than buttons
for _ in 1...numButtons+1 {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .red // just so we can see them... use .clear for production
spacers.append(v)
}
// addd spacers and buttons to stackView
for i in 0..<spacers.count {
stackView.addArrangedSubview(spacers[i])
// one fewer buttons than spacers, so don't go out-of-range
if i < buttons.count {
stackView.addArrangedSubview(buttons[i])
}
if i > 0 {
// constrain spacer widths to first spacer's width (this will make them all equal)
spacers[i].widthAnchor.constraint(equalTo: spacers[0].widthAnchor, multiplier: 1.0).isActive = true
// if you want the buttons to be equal widths, uncomment this block
/*
if i < buttons.count {
buttons[i].widthAnchor.constraint(equalTo: buttons[0].widthAnchor, multiplier: 1.0).isActive = true
}
*/
}
}
}
}
The results with 3 buttons:
and with 4 buttons:
and a couple with equal-width buttons:

Swift - How put a stack of views into a scrollview

Currently I have a custom view (returns a UIStakView) that contains many views (UILabel, UIImageView, ...). It displays fine - on devices with plenty of height.
(BTW, this is all done programmatically.)
On small-screen devices it will only show the top part of the entire view. So my solution is to place it inside a UIScrollView. (This should be simple - but it's giving me lots of grief.)
But this won't display at all, what am I doing wrong / have missed?
Partial code below:
override init(frame: CGRect)
{
super.init(frame: frame)
imageFrame.addSubview(prodImage)
NSLayoutConstraint.activate([
prodImage.topAnchor.constraint(equalTo: imageFrame.topAnchor),
prodImage.trailingAnchor.constraint(equalTo: imageFrame.trailingAnchor),
prodImage.leadingAnchor.constraint(equalTo: imageFrame.leadingAnchor),
prodImage.bottomAnchor.constraint(equalTo: imageFrame.bottomAnchor),
])
imageView.addSubview(imageFrame)
NSLayoutConstraint.activate([
imageFrame.topAnchor.constraint(equalTo: imageView.topAnchor),
imageFrame.trailingAnchor.constraint(equalTo: imageView.trailingAnchor),
imageFrame.leadingAnchor.constraint(equalTo: imageView.leadingAnchor),
imageFrame.bottomAnchor.constraint(equalTo: imageView.bottomAnchor),
])
// More views...
let stack = UIStackView(arrangedSubviews: [imageView, prodName, prodPrice])
stack.axis = .vertical
stack.spacing = (self.frame.height > 400) ? (self.frame.height > 800) ? 15 : 10 : 5
stack.distribution = UIStackViewDistribution.fill
self.addSubview(stack)
NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalTo: self.leadingAnchor),
stack.topAnchor.constraint(equalTo: self.topAnchor),
// stack.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -50),
stack.widthAnchor.constraint(equalTo: self.widthAnchor),
])
}
To make changes, I replaced the bottom stanza:
// self.addSubview(stack)
// NSLayoutConstraint.activate([
// stack.leadingAnchor.constraint(equalTo: self.leadingAnchor),
// stack.topAnchor.constraint(equalTo: self.topAnchor),
//// stack.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -50),
// stack.widthAnchor.constraint(equalTo: self.widthAnchor),
// ])
let scrollView = UIScrollView()
// scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(stack)
NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
stack.topAnchor.constraint(equalTo: scrollView.topAnchor),
stack.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -50),
stack.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
])
self.addSubview(scrollView)
NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
scrollView.topAnchor.constraint(equalTo: self.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -50),
scrollView.widthAnchor.constraint(equalTo: self.widthAnchor),
])
As you can see, I tried disabling auto-constraints for the scroll view to make it fit the it's parent... All attempts failed.
How can I make this scroll view visible?
Possible mistake:
You are setting the stack view's leading / trailing to the scroll view.
If you print the frame's you might understand that the width is zero
This is because that:
stack view's width can't be determined based on the scroll view.
scroll view is a special view because it's content can be larger than the scroll view.
so you need to explicitly set the content view's (stack view's) width
Possible Fix 1:
Instead of setting it based on the scrollView set it on the view (assuming scrollView is added as a subview to viewController's view)
stack.leadingAnchor.constraint(equalTo: view.leadingAnchor),
stack.topAnchor.constraint(equalTo: view.topAnchor),
Possible Fix 2:
You set the stack view's width anchor explicitly
Example:
Given below is a simple example of how to use stack view with the scroll view.
Your broad idea is correct.
Scroll View has a stack view
The stack view has a few subviews
Screen Shot:
General Explanation:
Scroll view is special because a scroll view's content can be wider and taller than the scroll view itself (allowing it to scroll)
So the content's width and height should not be tied to the scroll view
The content's width and height should be set without the scroll view having any part to play
Strategy
As you have pointed out, I like to use a Scroll view and content view
Add the actual content to the stack view and let the stack view grow
So as long as the stack view's constraints to the scroll view are set properly things should fall in place.
Debugging:
Always print the frame values in viewDidAppear to see if things match your expectation
Example Code:
class ViewController: UIViewController {
let scrollView = UIScrollView()
let contentView = UIStackView()
let redView = UIView()
let greenView = UIView()
let yellowView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
setupScrollView()
setupContentView()
setupRedView()
setupGreenView()
setupYellowView()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("scroll view = \(scrollView.frame)")
print("content view = \(contentView.frame)")
print("red view = \(redView.frame)")
print("green view = \(greenView.frame)")
print("yellow view = \(yellowView.frame)")
}
private func setupScrollView() {
scrollView.backgroundColor = .darkGray
view.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
scrollView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
}
private func setupContentView() {
contentView.axis = .vertical
contentView.distribution = .fill
contentView.alignment = .fill
scrollView.addSubview(contentView)
contentView.translatesAutoresizingMaskIntoConstraints = false
//Strategy is:
//content view's leading / trailing anchors are set to view controller's view
//content view's top / bottom anchors are set to scroll view
contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
contentView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
}
private func setupRedView() {
redView.backgroundColor = .red
redView.heightAnchor.constraint(equalToConstant: 400).isActive = true
contentView.addArrangedSubview(redView)
}
private func setupGreenView() {
greenView.backgroundColor = .green
greenView.heightAnchor.constraint(equalToConstant: 400).isActive = true
contentView.addArrangedSubview(greenView)
}
private func setupYellowView() {
yellowView.backgroundColor = .yellow
yellowView.heightAnchor.constraint(equalToConstant: 400).isActive = true
contentView.addArrangedSubview(yellowView)
}
}