Trouble getting UIImageView inside ScrollView programmatically - swift

After days of trying, and searching through countless SO/google/YouTube pages, I unclear how to accomplish this: I'm trying to place a single tall, narrow image inside a UIScrollView that only takes up a section of the screen, only scrolls vertically, and is preferably only coded programmatically, no Interface Builder at all.
I've managed to create the scrollView, set the backgroundColor to blue so I can see it and managed to use constraint anchors to pin it exactly where I need it to be. I then added the top and bottom labels as every video tutorial was telling me to, but I've since deleted these as they didn't seem necessary once I added the image.
The problems start as soon as I try to add the image. I've added an example image below as it's a tall, narrow image.
https://imgur.com/7qI1IaT
If you run the code with the image, you'll see:
The image scrolls horizontally as well as vertically. I'd have thought content.didOffset.x < 0 would work, but apparently not. There's probably a simple method to fix this but I'm yet to find it.
If the height of the image is less than the height of the scrollView, i want the image to stretch to fit the scrollView. I used both .scaleAspectFit and .scaleAspectFill and neither of these seemed to change anything.
The width of the image (or at least, the image I'm using, not the example image) is larger than the section of scrollView I have, and it goes off the screen. Again, I'm sure there's an easy fix to this, but I don't know.
Here is my code, but it's probably all wrong.
import UIKit
class ViewController: UIViewController {
lazy var scrollView: UIScrollView = {
let view = UIScrollView()
view.translatesAutoresizingMaskIntoConstraints = false
view.frame.size.height = 3000
view.backgroundColor = UIColor.blue
return view
}()
let imageView: UIImageView = {
let image = UIImageView(image: imageLiteral)
image.translatesAutoresizingMaskIntoConstraints = false
return image
}()
func setupLayout() {
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 100).isActive = true
scrollView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 100).isActive = true
scrollView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -10).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
scrollView.addSubview(imageView)
imageView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
imageView.leftAnchor.constraint(equalTo: scrollView.leftAnchor).isActive = true
imageView.rightAnchor.constraint(equalTo: scrollView.rightAnchor).isActive = true
imageView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
imageView.frame.size.height = scrollView.frame.size.height
imageView.frame.size.width = scrollView.frame.size.width
imageView.contentMode = .scaleAspectFit
imageView.clipsToBounds = true
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(scrollView)
setupLayout()
}
}
I don't know if I'm doing the right thing by adding the image as a subview of scrollView. I couldn't get the image to scroll at all until I changed it from the subview of view to scrollView. The labels in the tutorials I've seen were added that way, and it made more sense to me to add it into the scrollView than the main screen view, but again, this could be wrong.
I'm really not sure if it's the constraints, the contentSize or what, but it’s pretty clear I don't know what I’m doing, and I don't want to just wing it, so if anyone knows of any YouTube videos or websites that can help me out, I’d really appreciate it.
Again, apologies. I feel like this is a really simple fix, but I just don't have it.

There are a number of ways of accomplishing this, but I’d be inclined to set the zoomScale of the scroll view appropriate for this image view width, e.g.
// we want to make sure we adjust scale as views are laid out
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if imageView.intrinsicContentSize.width != 0 {
let scale = scrollView.bounds.width / imageView.intrinsicContentSize.width
scrollView.maximumZoomScale = scale
scrollView.minimumZoomScale = scale
scrollView.zoomScale = scale
}
}
To do that, you’ll have to set the delegate of the UIScrollView:
scrollView.delegate = self // we need to specify delegate so we can implement `viewForZooming(in:)`
And implement viewForZooming(in:):
extension ViewController: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
}
So pulling that all together:
class ViewController: UIViewController {
let sampleImage: UIImage = ...
let scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
// view.frame.size.height = 3000 // not needed as we're using constraints
scrollView.backgroundColor = .blue
return scrollView
}()
let imageView: UIImageView = {
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
// imageView.contentMode = .scaleAspectFit // not needed as we're going to let the intrinsic size dictate the size of the image view and therefore no scaling is happening
imageView.clipsToBounds = true
return imageView
}()
func setupLayout() {
view.addSubview(scrollView)
scrollView.addSubview(imageView)
imageView.image = sampleImage
scrollView.delegate = self // we need to specify delegate so we can implement `viewForZooming(in:)`
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 200),
scrollView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 100),
scrollView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -10),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
imageView.topAnchor.constraint(equalTo: scrollView.topAnchor),
imageView.leftAnchor.constraint(equalTo: scrollView.leftAnchor),
imageView.rightAnchor.constraint(equalTo: scrollView.rightAnchor),
imageView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor)
])
// these are not needed because we're using constraints
//
// imageView.frame.size.height = scrollView.frame.size.height
// imageView.frame.size.width = scrollView.frame.size.width
}
override func viewDidLoad() {
super.viewDidLoad()
setupLayout()
}
// we want to make sure we adjust scale as views are laid out
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if imageView.intrinsicContentSize.width != 0 {
let scale = scrollView.bounds.width / imageView.intrinsicContentSize.width
scrollView.maximumZoomScale = scale
scrollView.minimumZoomScale = scale
scrollView.zoomScale = scale
}
}
}
extension ViewController: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
}

You need to constraint the image width to the scrollview width. However you cannot do it directly, because the image is a subview of the scrollview and direct constraint would refer to width of the content of the scrollview not width. I have solved it by adding a layout guide that is constrained to the width of the scrollview "from the outside".
Also when you add constraint for the width you are left with intrinsic constraint for the height and that would change aspect ratio of the image. You need to add a constraint for the original aspect ratio.
Here is my code:
class ViewController: UIViewController {
let scrollView = UIScrollView()
let imageView = UIImageView(image: UIImage(named: "tallimage"))
let widthGuide = UILayoutGuide()
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
setupLayout()
}
func setupViews() {
scrollView.backgroundColor = UIColor.blue
view.addSubview(scrollView)
scrollView.addSubview(imageView)
view.addLayoutGuide(widthGuide)
}
func setupLayout() {
let ratio: CGFloat = (imageView.image?.size.height ?? 1) / (imageView.image?.size.width ?? 1)
imageView.translatesAutoresizingMaskIntoConstraints = false
scrollView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 100),
scrollView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 100),
scrollView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -10),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
imageView.topAnchor.constraint(equalTo: scrollView.topAnchor),
imageView.leftAnchor.constraint(equalTo: scrollView.leftAnchor),
imageView.rightAnchor.constraint(equalTo: scrollView.rightAnchor),
imageView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
// Make the image the same width as the scrollview.
widthGuide.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
imageView.widthAnchor.constraint(equalTo: widthGuide.widthAnchor),
// Keep the height/width ratio of the image so it is not deformed.
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: ratio),
])
}
}
I have also changed style of the code. Feel free to use your original style.
However, I like to
When I call a method, it is defined below the line where it is used (in order).
Use NSLayoutConstraint.activate() when activating more constraints.
Use simple instance variables (let constants) and configure them later.

Related

Stretchy Header with UIPageViewController

My problem seems obvious and duplicated but I can't manage to make it work.
I'm trying to achieve the famous stretchy header effect (image's top side stuck to top of UIScrollView when scrolling), but with an UIPageViewController instead of simply an image.
My structure is:
UINavigationBar
|-- UIScrollView
|-- UIView (totally optional container)
|-- UIPageViewController (as UIView, embedded with addChild()) <-- TO STICK
|-- UIHostingViewController (SwiftUI view with labels, also embedded)
|-- UITableView (not embedded but could be)
My UIPageViewController contains images to make a carousel, nothing more.
All my views are laid out with NSLayoutConstraints (with visual format for vertical layout in the container).
I trie sticking topAnchor of the page controller's view to the one of self.view (with or without priority) but no luck, and no matter what I do it changes absolutely nothing.
I finally tried to use SnapKit but it doesn't work neither (I don't know much about it but it seems to only be a wrapper for NSLayoutConstaints so I'm not surprised it doesn't work too).
I followed this tutorial, this one and that one but none of them worked.
(How) can I achieve what I want?
EDIT 1:
To clarify, my carousel currently has a forced height of 350. I want to achieve this exact effect (that is shown with a single UIImageView) on my whole carousel:
To clarify as much as possible, I want to replicate this effect to my whole UIPageViewController/carousel so that the displayed page/image can have this effect when scrolled.
NOTE: as mentioned in the structure above, I have a (transparent) navigation bar, and my safe area insets are respected (nothing goes under the status bar). I don't think it would change the solution (as the solution is probably a way to stick the top of the carousel to self.view, no matter the frame of self.view) but I prefer you to know everything.
EDIT 2:
Main VC with #DonMag's answer:
private let info: UITableView = {
let v = UITableView(frame: .zero, style: .insetGrouped)
v.backgroundColor = .systemBackground
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
private lazy var infoHeightConstraint: NSLayoutConstraint = {
// Needed constraint because else standalone UITableView gets an height of 0 even with usual constraints
// I update this constraint in viewWillAppear & viewDidAppear when the table gets a proper contentSize
info.heightAnchor.constraint(equalToConstant: 0.0)
}()
private let scrollView: UIScrollView = {
let v = UIScrollView()
v.contentInsetAdjustmentBehavior = .never
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
...
// MARK: Views declaration
// Container for carousel
let stretchyView = UIView()
stretchyView.translatesAutoresizingMaskIntoConstraints = false
// Carousel
let carouselController = ProfileDetailCarousel(images: [
UIImage(named: "1")!,
UIImage(named: "2")!,
UIImage(named: "3")!,
UIImage(named: "4")!
])
addChild(carouselController)
let carousel: UIView = carouselController.view
carousel.translatesAutoresizingMaskIntoConstraints = false
stretchyView.addSubview(carousel)
carouselController.didMove(toParent: self)
// Container for below-carousel views
let contentView = UIView()
contentView.translatesAutoresizingMaskIntoConstraints = false
// Texts and bio
let bioController = UIHostingController(rootView: ProfileDetailBio())
addChild(bioController)
let bio: UIView = bioController.view
bio.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(bio)
bioController.didMove(toParent: self)
// Info table
info.delegate = tableDelegate
info.dataSource = tableDataSource
tableDelegate.viewController = self
contentView.addSubview(info)
[stretchyView, contentView].forEach { v in
scrollView.addSubview(v)
}
view.addSubview(scrollView)
// MARK: Constraints
let stretchyTop = stretchyView.topAnchor.constraint(equalTo: scrollView.frameLayoutGuide.topAnchor)
stretchyTop.priority = .defaultHigh
NSLayoutConstraint.activate([
// Scroll view
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
// Stretchy view
stretchyTop,
stretchyView.leadingAnchor.constraint(equalTo: scrollView.frameLayoutGuide.leadingAnchor),
stretchyView.trailingAnchor.constraint(equalTo: scrollView.frameLayoutGuide.trailingAnchor),
stretchyView.heightAnchor.constraint(greaterThanOrEqualToConstant: 350.0),
// Carousel
carousel.topAnchor.constraint(equalTo: stretchyView.topAnchor),
carousel.bottomAnchor.constraint(equalTo: stretchyView.bottomAnchor),
carousel.centerXAnchor.constraint(equalTo: stretchyView.centerXAnchor),
carousel.widthAnchor.constraint(equalTo: stretchyView.widthAnchor),
// Content view
contentView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
contentView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor),
contentView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: 350.0),
contentView.topAnchor.constraint(equalTo: stretchyView.bottomAnchor),
// Bio
bio.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10.0),
bio.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
bio.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
bio.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
// Info table
info.topAnchor.constraint(equalTo: bio.bottomAnchor, constant: 12.0),
info.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
info.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
infoHeightConstraint
])
}
Your view hierarchy should be:
UINavigationBar
|-- UIScrollView
|-- UIView ("stretchy" container view)
|-- UIPageViewController (as UIView, embedded with asChild())
|-- UIHostingViewController (SwiftUI view with labels, also embedded)
To get the stretchy view to "stick to the top":
We constrain the stretchy view's Top to the scroll view's .frameLayoutGuide Top, but we give that constraint a less-than-required .priority so we can "push it" up and off the screen.
We also give the stretchy view a Height constraint of greater-than-or-equal-to 350. This will allow it to stretch - but not compress - vertically.
We'll call the view from the UIHostingViewController our "contentView" ... and we'll constrain its Top to the stretchy view's Bottom.
Then, we give the content view another Top constraint -- this time to the scroll view's .contentLayoutGuide, with a constant of 350 (the height of the stretchy view). This, plus the Leading/Trailing/Bottom constraints defines the "scrollable area."
When we scroll (pull) down, the content view will "pull down" the Bottom of the stretchy view.
When we scroll (push) up, the content view will "push up" the entire stretchy view.
Here's how it looks (too big to add as a gif here): https://imgur.com/a/wkThhzN
And here's the sample code to make that. Everything is done via code, so no #IBOutlet or other connections needed. Also note that I used three images for the page views - "ex1", "ex2", "ex3":
View Controller
class StretchyHeaderViewController: UIViewController {
let scrollView: UIScrollView = {
let v = UIScrollView()
v.contentInsetAdjustmentBehavior = .never
return v
}()
let stretchyView: UIView = {
let v = UIView()
return v
}()
let contentView: UIView = {
let v = UIView()
v.backgroundColor = .systemYellow
return v
}()
let stretchyViewHeight: CGFloat = 350.0
override func viewDidLoad() {
super.viewDidLoad()
// set to a greter-than-zero value if you want spacing between the "pages"
let opts = [UIPageViewController.OptionsKey.interPageSpacing: 0.0]
// instantiate the Page View controller
let pgVC = SamplePageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: opts)
// add it as a child controller
self.addChild(pgVC)
// safe unwrap
guard let pgv = pgVC.view else { return }
pgv.translatesAutoresizingMaskIntoConstraints = false
// add the page controller view to stretchyView
stretchyView.addSubview(pgv)
pgVC.didMove(toParent: self)
NSLayoutConstraint.activate([
// constrain page view controller's view on all 4 sides
pgv.topAnchor.constraint(equalTo: stretchyView.topAnchor),
pgv.bottomAnchor.constraint(equalTo: stretchyView.bottomAnchor),
pgv.centerXAnchor.constraint(equalTo: stretchyView.centerXAnchor),
pgv.widthAnchor.constraint(equalTo: stretchyView.widthAnchor),
])
[scrollView, stretchyView, contentView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
// add contentView and stretchyView to the scroll view
[stretchyView, contentView].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
// keep stretchyView's Top "pinned" to the Top of the scroll view FRAME
// so its Height will "stretch" when scroll view is pulled down
let stretchyTop = stretchyView.topAnchor.constraint(equalTo: frameG.topAnchor, constant: 0.0)
// priority needs to be less-than-required so we can "push it up" out of view
stretchyTop.priority = .defaultHigh
NSLayoutConstraint.activate([
// scroll view Top to view Top
scrollView.topAnchor.constraint(equalTo: view.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 stretchy view Top to scroll view's FRAME
stretchyTop,
// stretchyView to Leading/Trailing of scroll view FRAME
stretchyView.leadingAnchor.constraint(equalTo: frameG.leadingAnchor, constant: 0.0),
stretchyView.trailingAnchor.constraint(equalTo: frameG.trailingAnchor, constant: 0.0),
// stretchyView Height - greater-than-or-equal-to
// so it can "stretch" vertically
stretchyView.heightAnchor.constraint(greaterThanOrEqualToConstant: stretchyViewHeight),
// content view Leading/Trailing/Bottom to scroll view's CONTENT GUIDE
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),
// content view Top to scroll view's CONTENT GUIDE
// plus Height of stretchyView
contentView.topAnchor.constraint(equalTo: contentG.topAnchor, constant: stretchyViewHeight),
// content view Top to stretchyView Bottom
contentView.topAnchor.constraint(equalTo: stretchyView.bottomAnchor, constant: 0.0),
])
// add some content to the content view so we have something to scroll
addSomeContent()
}
func addSomeContent() {
// vertical stack view with 20 labels
// 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),
])
}
}
Controller for each Page
class OnePageVC: UIViewController {
var image: UIImage = UIImage() {
didSet {
imgView.image = image
}
}
let imgView: UIImageView = {
let v = UIImageView()
v.backgroundColor = .systemBlue
v.contentMode = .scaleAspectFill
v.clipsToBounds = true
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
view.addSubview(imgView)
NSLayoutConstraint.activate([
// constrain image view to all 4 sides
imgView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0.0),
imgView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0.0),
imgView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0.0),
imgView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0.0),
])
}
}
Sample Page View Controller
class SamplePageViewController: UIPageViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource {
var controllers: [UIViewController] = []
override func viewDidLoad() {
super.viewDidLoad()
let imgNames: [String] = [
"ex1", "ex2", "ex3",
]
for i in 0..<imgNames.count {
let aViewController = OnePageVC()
if let img = UIImage(named: imgNames[i]) {
aViewController.image = img
}
self.controllers.append(aViewController)
}
self.dataSource = self
self.delegate = self
self.setViewControllers([controllers[0]], direction: .forward, animated: false)
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
if let index = controllers.firstIndex(of: viewController), index > 0 {
return controllers[index - 1]
}
return nil
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
if let index = controllers.firstIndex(of: viewController), index < controllers.count - 1 {
return controllers[index + 1]
}
return nil
}
}
Edit
Looking at the code you posted in your question's Edit... it's a little tough, since I don't know what your ProfileDetailBio view is, but here are a couple tips to help debug this type of situation during development:
give your views contrasting background colors... makes it easy to see the frames when you run the app
if a subview fills its superview's width, make it a little narrower so you can see what's "behind / under" it
set .clipsToBounds = true on views you're using as "containers" - such as contentView... if a subview is then "missing" you know it has extended outside the bounds of the container
So, for your code...
// so we can see the contentView frame
contentView.backgroundColor = .systemYellow
// leave some space on the right-side of bio view, so we
// so we can see the contentView behind it
bio.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -100.0),
If you run the app, you will likely see that contentView only extends to the bottom of bio - not to the bottom of info.
If you then do this:
contentView.clipsToBounds = true
info will likely not be visible at all.
Checking your constraints, you have:
bio.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
// Info table
info.topAnchor.constraint(equalTo: bio.bottomAnchor, constant: 12.0),
where it should be:
// no bio bottom anchor
//bio.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
// this is correct
// Info table
info.topAnchor.constraint(equalTo: bio.bottomAnchor, constant: 12.0),
// add this
info.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
Run the app, and you should now again see info, and contentView extends to the bottom of info.
Assuming bio and info height combined are tall enough to require scrolling, you can undo the "debug / dev" changes and you should be good to go.

NSLayoutConstraints not cooperating with UIImageView

In my UIView subclass, I have one image view and three labels:
let imageView = UIImageView()
let firstLabel = UILabel()
let secondLabel = UILabel()
let thirdLabel = UILabel()
The image and texts are set by the view controller that uses the view.
I begin to set them up with:
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
addSubview(imageView)
firstLabel.translatesAutoresizingMaskIntoConstraints = false
firstLabel.textAlignment = .center
addSubview(firstLabel)
secondLabel.translatesAutoresizingMaskIntoConstraints = false
secondLabel.textAlignment = .center
addSubview(secondLabel)
thirdLabel.translatesAutoresizingMaskIntoConstraints = false
thirdLabel.textAlignment = .center
addSubview(thirdLabel)
I am trying to constrain these in such a way such that it looks like the following (rough drawing):
Specifically:
thirdLabel is in the center at the bottom
secondLabel is in the center directly above thirdLabel
firstLabel is in the center directly above secondLabel
The size of imageView will vary depending on the size of the view, however it must meet these criteria:
It is in the center directly above firstLabel
It reaches the top
It is a square
So if the height of the view was larger, only the image view would enlarge, the labels would NOT increase height and evenly space out. They would remain at the bottom. So visually, this would be good:
and this would be bad:
An example of what I've tried (one of MANY):
NSLayoutConstraint.activate([
thirdLabel.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor),
thirdLabel.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor),
thirdLabel.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor),
thirdLabel.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor),
secondLabel.centerXAnchor.constraint(equalTo: thirdLabel.centerXAnchor),
secondLabel.bottomAnchor.constraint(equalTo: thirdLabel.topAnchor),
secondLabel.leadingAnchor.constraint(equalTo: thirdLabel.leadingAnchor),
secondLabel.trailingAnchor.constraint(equalTo: thirdLabel.trailingAnchor),
firstLabel.centerXAnchor.constraint(equalTo: secondLabel.centerXAnchor),
firstLabel.bottomAnchor.constraint(equalTo: secondLabel.topAnchor),
firstLabel.leadingAnchor.constraint(equalTo: secondLabel.leadingAnchor),
firstLabel.trailingAnchor.constraint(equalTo: secondLabel.trailingAnchor),
imageView.centerXAnchor.constraint(equalTo: firstLabel.centerXAnchor),
imageView.bottomAnchor.constraint(equalTo: firstLabel.topAnchor),
imageView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor),
])
I've mixed and matched so many constraints but I cannot achieve the layout in the first image. Not only can I get it working with various heights, I can't even get it to work with ANY height. Sometimes the image view takes up the whole thing and I can't even see the labels (are they underneath the view? behind the image view?). Sometimes the height of the labels are increased. These things occur even though I have constraints that seemingly don't allow this to happen? No breaking of constraint messages appear in the console either.
I believe it may have something to do with sizing, because if I don't set an image (and set a background color for imageView so I can see where it is), it works perfectly. It's only when I actually assign an image to imageView.image do things act up. I've tried resizing the image beforehand, along with setting many variables and constraints not shown in the particular example given above.
Frustrating!
You need to set both Content Compression Resistance and Content Hugging priorities on your labels.
Here is an example custom view class (using mostly your code):
class AJPView: UIView {
let imageView = UIImageView()
let firstLabel = UILabel()
let secondLabel = UILabel()
let thirdLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
addSubview(imageView)
firstLabel.translatesAutoresizingMaskIntoConstraints = false
firstLabel.textAlignment = .center
addSubview(firstLabel)
secondLabel.translatesAutoresizingMaskIntoConstraints = false
secondLabel.textAlignment = .center
addSubview(secondLabel)
thirdLabel.translatesAutoresizingMaskIntoConstraints = false
thirdLabel.textAlignment = .center
addSubview(thirdLabel)
NSLayoutConstraint.activate([
thirdLabel.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor),
thirdLabel.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor),
thirdLabel.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor),
secondLabel.bottomAnchor.constraint(equalTo: thirdLabel.topAnchor),
secondLabel.leadingAnchor.constraint(equalTo: thirdLabel.leadingAnchor),
secondLabel.trailingAnchor.constraint(equalTo: thirdLabel.trailingAnchor),
firstLabel.bottomAnchor.constraint(equalTo: secondLabel.topAnchor),
firstLabel.leadingAnchor.constraint(equalTo: secondLabel.leadingAnchor),
firstLabel.trailingAnchor.constraint(equalTo: secondLabel.trailingAnchor),
imageView.centerXAnchor.constraint(equalTo: firstLabel.centerXAnchor),
imageView.bottomAnchor.constraint(equalTo: firstLabel.topAnchor),
imageView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor),
// you've given the labels leading and trailing constraints,
// so you don't need these
//thirdLabel.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor),
//secondLabel.centerXAnchor.constraint(equalTo: thirdLabel.centerXAnchor),
//firstLabel.centerXAnchor.constraint(equalTo: secondLabel.centerXAnchor),
])
// prevent labels from being compressed or stretched vertically
[firstLabel, secondLabel, thirdLabel].forEach {
$0.setContentCompressionResistancePriority(.required, for: .vertical)
$0.setContentHuggingPriority(.required, for: .vertical)
}
// let's give the subviews background colors
// so we can easily see the frames
let clrs: [UIColor] = [
.systemYellow,
.green,
.cyan,
.yellow
]
for (v, c) in zip([imageView, firstLabel, secondLabel, thirdLabel], clrs) {
v.backgroundColor = c
}
}
}
and a demo view controller:
class ViewController: UIViewController {
var heightConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
let testView = AJPView()
testView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testView)
let g = view.safeAreaLayoutGuide
heightConstraint = testView.heightAnchor.constraint(equalToConstant: 120.0)
NSLayoutConstraint.activate([
testView.widthAnchor.constraint(equalToConstant: 300.0),
testView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
testView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
// activate height anchor
heightConstraint,
])
testView.firstLabel.text = "First"
testView.secondLabel.text = "Second"
testView.thirdLabel.text = "Third"
if let img = UIImage(named: "myImage") {
testView.imageView.image = img
} else {
if let img = UIImage(systemName: "person.circle.fill") {
testView.imageView.image = img
}
}
// so we can see the frame of the view
testView.layer.borderWidth = 1
testView.layer.borderColor = UIColor.red.cgColor
// add grow / shrink buttons
let stack = UIStackView()
stack.translatesAutoresizingMaskIntoConstraints = false
stack.spacing = 20
stack.distribution = .fillEqually
["Taller", "Shorter"].forEach {
let b = UIButton(type: .system)
b.backgroundColor = .yellow
b.setTitle($0, for: [])
b.addTarget(self, action: #selector(btnTapped(_:)), for: .touchUpInside)
stack.addArrangedSubview(b)
}
view.addSubview(stack)
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
stack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
stack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
])
}
#objc func btnTapped(_ sender: UIButton) -> Void {
var h = heightConstraint.constant
if sender.currentTitle == "Taller" {
h += 10
} else {
h -= 10
}
heightConstraint.constant = h
}
}
The output looks like this (the custom view is outlined in red):
you can tap the "Taller" / "Shorter" buttons to make the custom view grow or shrink (by 10-pts each tap) to see the changes:
Note that the view will eventually get too tall for the 1:1 ratio image view to fit horizontally:

How to use automatically/dynamically set scrollview to fit the content view

Surprisingly, this is harder than I thought. I followed this tutorial which seems rather straightforward but I am programmatically creating my view instead of using storyboard. Just to be clear, the content I add to the content view is static i.e. it's not growing or increasing.
Here are the definitions of scroll view and content view:
lazy var contentView : UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
lazy var scrollView : UIScrollView = {
let scrollView = UIScrollView(frame: .zero)
scrollView.backgroundColor = .white
scrollView.frame = self.view.bounds
scrollView.bounces = true
scrollView.autoresizingMask = .flexibleHeight
scrollView.contentSize = CGSize(width: self.view.frame.width, height: contentView.frame.height)
scrollView.translatesAutoresizingMaskIntoConstraints = false
return scrollView
}()
In view did load add the scroll view and set its constraints in the view controller:
view.addSubview(scrollView)
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
Then I add content view and the constraints:
scrollView.addSubview(contentView)
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
contentView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
let constraint = contentView.heightAnchor.constraint(equalTo: view.heightAnchor)
constraint.priority = UILayoutPriority(250)
constraint.isActive = true
The problem is that I cannot scroll all the way to the bottom.
Initial points to note
Delete
scrollView.frame = self.view.bounds
It is pointless to give scrollView a frame, as you are going to give it a frame later through the use of constraints.
Delete
scrollView.autoresizingMask = .flexibleHeight
You are using constraints, not the autoresizing mask, to give the scroll view its frame and later resizing behavior.
Delete
scrollView.contentSize = CGSize(width: self.view.frame.width, height: contentView.frame.height)
Once the scroll view is under the influence of constraints, you must use constraints, not contentSize, to give it a content size that determines scroll behavior.
Adding the content view
With those preliminaries out of the way, let's talk about how you add the content view to the scroll view:
scrollView.addSubview(contentView)
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
That is not exactly wrong, but it is very outmoded. You should pin the content view to the scroll view's content layout guide; that is what it is for. So, wherever you have equalTo: scrollView, change it to equalTo: scrollView.contentLayoutGuide.
Scrolling
Okay! Now everything is assembled and we are ready to talk about scrolling. What makes the scroll view scrollable, in this configuration, is that the content view is bigger than the scroll view itself. Well, so far, that's not true; in fact, the content view has no size at all. So we must proceed to give it some size.
Your attempt to do that is rather feeble. Let's extract the key lines where you give the content view height and width constraints:
contentView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
contentView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
(Delete the other two lines, as they are not really doing anything useful now.) So now is the content view any bigger than the scroll view? Well, maybe, but if so, only by a tiny bit, because the content view is merely the size of the main view, and the scroll view is either that same size or a tiny bit smaller.
Since we are just demonstrating, it would be better to make the content view a lot bigger than the scroll view, so we can do some major scrolling. Change the second line to this:
contentView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier:2 ).isActive = true
Yeah, baby! Now we can really scroll.
Making the content more visible
Still, it's a little hard to see what's happening (everything is white on white), so I suggest you fill the content view with some color that will permit us to see what's going on. Declare a self-drawing view as follows:
class MyView : UIView {
override class var layerClass : AnyClass { CAGradientLayer.self }
override func willMove(toSuperview newSuperview: UIView?) {
let lay = self.layer as! CAGradientLayer
lay.colors = [UIColor.red.cgColor, UIColor.green.cgColor]
}
}
Now change
let view = UIView()
to
let view = MyView()
Now it is very obvious when you scroll to the bottom; the real green is visible at the bottom.
Summary
Here is the complete code of the corrected example:
class MyView : UIView {
override class var layerClass : AnyClass { CAGradientLayer.self }
override func willMove(toSuperview newSuperview: UIView?) {
let lay = self.layer as! CAGradientLayer
lay.colors = [UIColor.red.cgColor, UIColor.green.cgColor]
}
}
class ViewController: UIViewController {
lazy var contentView : UIView = {
let view = MyView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
lazy var scrollView : UIScrollView = {
let scrollView = UIScrollView(frame: .zero)
scrollView.backgroundColor = .white
scrollView.bounces = true
scrollView.translatesAutoresizingMaskIntoConstraints = false
return scrollView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(scrollView)
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
scrollView.addSubview(contentView)
contentView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor).isActive = true
contentView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor).isActive = true
contentView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor).isActive = true
contentView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
contentView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier:2 ).isActive = true
}
}

Using UIScrollView with a minimum content top anchor causes visual glitch

I have a scroll view in which I have a content view. I set the scroll view's top anchor to be just above the bottom of an image. I set the content view's top anchor to actually be at the bottom of the image. That way you can pull down on the content and reveal up to the bottom of the image without being able to pull the content view down any further. However, this is causing the content to jump.
Here is my code:
class HomeParallaxScrollViewController: UIViewController {
private let topImageView = UIImageView(image: UIImage(named: "cat"))
private let contentView = UIView()
private let scrollView = UIScrollView()
private let label = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .gray
topImageView.contentMode = .scaleAspectFill
contentView.backgroundColor = .white
label.text = "SOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT"
label.textColor = .black
label.numberOfLines = 0
[contentView, label, topImageView, scrollView].forEach { $0.translatesAutoresizingMaskIntoConstraints = false }
scrollView.addSubview(contentView)
contentView.addSubview(label)
view.addSubview(topImageView)
view.addSubview(scrollView)
NSLayoutConstraint.activate([
topImageView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
topImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
topImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
topImageView.heightAnchor.constraint(equalToConstant: 200),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.widthAnchor.constraint(equalTo: view.widthAnchor),
scrollView.topAnchor.constraint(equalTo: topImageView.bottomAnchor, constant: -30),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
contentView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
contentView.topAnchor.constraint(lessThanOrEqualTo: topImageView.bottomAnchor), //This is what's causing the glitch
label.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
label.topAnchor.constraint(equalTo: contentView.topAnchor),
label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
}
}
And here is that is happening:
Trying to add another top constraint -- particularly to an element outside the scroll view -- is a bad idea, and, as you see, won't work. I'm sure you noticed auto-layout conflict messages being generated.
One approach is to implement scrollViewDidScroll delegate func:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// limit drag-down in the scroll view to the overlap size
scrollView.contentOffset.y = max(scrollView.contentOffset.y, -30)
}
As the user drags-down to scroll, it will stop at 30-points.
Here is your example, with slight modifications -- I don't have your .plBackgroundLightGray or .PLSemiboldFont and I added an image load for the top image view -- but this should run as-is:
// conform to UIScrollViewDelegate
class HomeParallaxScrollViewController: UIViewController, UIScrollViewDelegate {
private let topImageView = UIImageView(image: UIImage(named: "cat"))
private let contentView = UIView()
private let scrollView = UIScrollView()
private let label = UILabel()
// this will be the "overlap" of the scroll view and top image view
private var scrollOverlap: CGFloat = 30.0
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// limit drag-down in the scroll view to scrollOverlap points
scrollView.contentOffset.y = max(scrollView.contentOffset.y, -scrollOverlap)
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .lightGray // .plBackgroundLightGray
topImageView.contentMode = .scaleAspectFill
if let img = UIImage(named: "background") {
topImageView.image = img
}
contentView.backgroundColor = .white
label.text = "SOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT\n\n\nSOME\n\n\nRANDOM\n\n\nCONTENT"
label.font = UIFont.boldSystemFont(ofSize: 16) // .PLSemiboldFont(size: 16)
label.textColor = .black
label.numberOfLines = 0
[contentView, label, topImageView, scrollView].forEach { $0.translatesAutoresizingMaskIntoConstraints = false }
scrollView.addSubview(contentView)
contentView.addSubview(label)
view.addSubview(topImageView)
view.addSubview(scrollView)
NSLayoutConstraint.activate([
topImageView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
topImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
topImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
topImageView.heightAnchor.constraint(equalToConstant: 200),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.widthAnchor.constraint(equalTo: view.widthAnchor),
scrollView.topAnchor.constraint(equalTo: topImageView.bottomAnchor, constant: scrollOverlap),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
contentView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
// nope, not a good idea -- will cause constraint conflicts
//contentView.topAnchor.constraint(lessThanOrEqualTo: topImageView.bottomAnchor), //This is what's causing the glitch
label.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
label.topAnchor.constraint(equalTo: contentView.topAnchor),
label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
// set delegate to self
scrollView.delegate = self
}
}

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