UIScrollView with vertical stackview scrolls horizontally but not vertically - swift

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.

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.

Swift: Align view X points from center towards trailing edge

I have a UIImageView which should be placed 10 points from center of the parent UIView. In LTR it works fine but in RTL it should be -10 points.
Is there a way to align X points from center towards trailing edge? Or should I set the constraint manually based on the layout direction?
If you constrain the Leading anchor of the image view to the CenterX anchor of your view, with a Constant: 10, it will automatically swap to its Trailing anchor at -10 pts from the CenterX.
Here's how you could do it via code:
let imgView = UIImageView()
imgView.backgroundColor = .systemYellow
imgView.translatesAutoresizingMaskIntoConstraints = false
if let img = UIImage(systemName: "person") {
imgView.image = img
}
view.addSubview(imgView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
imgView.topAnchor.constraint(equalTo: g.topAnchor, constant: 100.0),
imgView.widthAnchor.constraint(equalToConstant: 120.0),
imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor),
imgView.leadingAnchor.constraint(equalTo: g.centerXAnchor, constant: 10.0),
])
Here's how it could look in Storyboard (the vertical red line is just a 1-pt wide view showing the horizontal center):
and at run-time, using RTL language (NO code at all):
translatesAutoresizingMaskIntoConstraints is used when you want to adjust Constrains to code.
You should also add Constrain for the top and bottom.
#IBOutlet weak var myImageView: UIImageView!
...
override func viewDidLoad() {
super.viewDidLoad()
myImageView.translatesAutoresizingMaskIntoConstraints = false
myImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0).isActive = true
myImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10).isActive = true
myImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 10).isActive = true
}
...

How to resize content view in UIScrollView programmatically?

Here is the issue: I need to add a view containing other subviews to a scroll view programmatically. In addition, I also need to make the frame of such a view to stick to the bounds of the main super view. The code below shows the approach I was trying to implement, but as you can see from the pictures below the 'contentView' is not updating its frame size when the screen is rotated. The initial code is taken from here for demonstration purposes. Any help would be greatly appreciated.
import UIKit
class TestViewController : UIViewController {
var contentViewSize = CGSize()
let contentView: UIView = {
let view = UIView()
view.backgroundColor = .magenta
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
let labelOne: UILabel = {
let label = UILabel()
label.text = "Scroll Top"
label.backgroundColor = .red
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let labelTwo: UILabel = {
let label = UILabel()
label.text = "Scroll Bottom"
label.backgroundColor = .green
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let scrollView: UIScrollView = {
let v = UIScrollView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .cyan
return v
}()
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
contentViewSize = view.bounds.size
labelTwo.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: (contentViewSize.width - labelTwo.frame.size.width - 16.0)).isActive = true
labelTwo.topAnchor.constraint(equalTo: contentView.topAnchor, constant: (contentViewSize.height - labelTwo.frame.size.height - 16.0)).isActive = true
}
override func viewDidLoad() {
super.viewDidLoad()
contentViewSize = view.bounds.size
view.backgroundColor = .yellow
self.view.addSubview(scrollView)
scrollView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 8.0).isActive = true
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 8.0).isActive = true
scrollView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -8.0).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8.0).isActive = true
scrollView.addSubview(contentView)
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 16).isActive = true
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 16).isActive = true
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 16).isActive = true
contentView.rightAnchor.constraint(equalTo: scrollView.rightAnchor, constant: 16).isActive = true
contentView.addSubview(labelOne)
labelOne.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16.0).isActive = true
labelOne.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16.0).isActive = true
contentView.addSubview(labelTwo)
labelTwo.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: (contentViewSize.width - labelTwo.frame.size.width - 16.0)).isActive = true
labelTwo.topAnchor.constraint(equalTo: contentView.topAnchor, constant: (contentViewSize.height - labelTwo.frame.size.height - 16.0)).isActive = true
labelTwo.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16.0).isActive = true
labelTwo.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16.0).isActive = true
}
}
You made some odd changes to the code from the answer you linked to. Also, that answer is a little out-of-date.
Here's a better example. Assuming you want only vertical scrolling, this will:
add a Cyan scroll view, inset 8-pts on each side from the safe-area
add a Magenta "content view" to the scroll view, with 16-pts on each side constrained to the scroll view's contentLayoutGuide, with a width 32-pts less than the scroll view's frame (16-pts on each side)
add a label at top-left of the content view
add a label at bottom-right of the content view
constrain the bottom label 1500-pts below the top label (so it will scroll vertically)
Code:
class ScrollTestViewController : UIViewController {
let contentView: UIView = {
let view = UIView()
view.backgroundColor = .magenta
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
let labelOne: UILabel = {
let label = UILabel()
label.text = "Scroll Top"
label.backgroundColor = .red
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let labelTwo: UILabel = {
let label = UILabel()
label.text = "Scroll Bottom"
label.backgroundColor = .green
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let scrollView: UIScrollView = {
let v = UIScrollView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .cyan
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .yellow
// add the scroll view
self.view.addSubview(scrollView)
// add contentView to scroll view
scrollView.addSubview(contentView)
// add two labels to contentView
contentView.addSubview(labelOne)
contentView.addSubview(labelTwo)
// respect safe-area
let g = view.safeAreaLayoutGuide
// if you want to ignore the safe-area (bad idea),
// use this instead
//let g = view!
//scrollView.contentInsetAdjustmentBehavior = .never
// we're going to constrain the contentView to the scroll view's content layout guide
let scg = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
// constrain scrollView Top / Leading / Trailing / Bottom to view (safe-area)
// with 8-pts on each side
scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 8.0),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -8.0),
// constrain contentView Top / Leading / Trailing / Bottom to scroll view's Content Layout Guide
// with 16-pts on each side
contentView.topAnchor.constraint(equalTo: scg.topAnchor, constant: 16.0),
contentView.leadingAnchor.constraint(equalTo: scg.leadingAnchor, constant: 16.0),
contentView.trailingAnchor.constraint(equalTo: scg.trailingAnchor, constant: -16.0),
contentView.bottomAnchor.constraint(equalTo: scg.bottomAnchor, constant: -16.0),
// if we only want vertical scrolling, constrain contentView Width
// to scrollView's Frame Layout Guide minus 32-pts (because we have 16-pts on each side)
contentView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -32.0),
// constrain labelOne Top / Leading 16-pts to contentView Top / Leading
// (so it shows up at top-left corner)
labelOne.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16.0),
labelOne.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16.0),
// constrain labelTwo Bottom / Trailing 16-pts to contentView Bottom / Trailing
// (so it shows up at bottom-right corner)
labelTwo.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16.0),
labelTwo.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16.0),
// constrain labelTwo Top to labelOne Bottom + 1500-pts
// so we'll have some vertical scrolling to get to it
labelTwo.topAnchor.constraint(equalTo: labelOne.bottomAnchor, constant: 1500.0),
])
}
}

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

UIView resize to fit labels inside of it

I have a UIView that I'm appending into a stack view on my main page of my app
This is the class of my view:
class MyCustomView: UIView {
public let leftLabel: UILabel = UILabel(frame: .zero)
public let rightLabel: UILabel = UILabel(frame: .zero)
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(leftLabel)
addSubview(rightLabel)
leftLabel.translatesAutoresizingMaskIntoConstraints = false
rightLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
leftLabel.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.4),
leftLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
leftLabel.topAnchor.constraint(equalTo: topAnchor),
leftLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
rightLabel.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.6),
rightLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
rightLabel.topAnchor.constraint(equalTo: topAnchor),
rightLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
])
leftLabel.text = "Short string"
rightLabel.text = "Short string too"
}
}
And I append to my main stack view with:
let myCustomView = MyCustomView(frame: .zero)
stackView.addArrangedSubview(myCustomView)
This loads in my label's correctly and resizes everything as I'd want.
However, in my main class, I am updating myCustomView.rightLabel.text = <New Way Longer Text That Takes 2 Lines Instead of One>
The text is updating properly, but my myCustomView size is not resizing, so part of the text is just being cut-off
I have tried following other answers on here, but none of them seem to work for me.
Am I missing something small in order to force the resize of the customView to fit the label inside of it?
Thank you in advance
Your code does not show that you set the .numberOfLines in the label(s) to 0 to allow for multi-line labels.
Adding only that, should allow your labels to grow in height and to expand your custom view. However... that will also make both labels expand to the size of the tallest label, resulting in the text of the "shorter" label being vertically centered (I added background colors to make it easy to see the frames / bounds of the views):
If you constrain the Bottom of your custom view to the Bottom of each label at greaterThanOrEqualTo you can keep the labels "top-aligned":
You can run this code directly in a Playground page to see the results:
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class MyCustomView: UIView {
public let leftLabel: UILabel = UILabel(frame: .zero)
public let rightLabel: UILabel = UILabel(frame: .zero)
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(leftLabel)
addSubview(rightLabel)
// background colors so we can see the view frames
backgroundColor = .cyan
leftLabel.backgroundColor = .yellow
rightLabel.backgroundColor = .green
// we want multi-line labels
leftLabel.numberOfLines = 0
rightLabel.numberOfLines = 0
// use auto-layout
leftLabel.translatesAutoresizingMaskIntoConstraints = false
rightLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
// constrain to top
leftLabel.topAnchor.constraint(equalTo: topAnchor),
// constrain to left
leftLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
// constrain width = 40%
leftLabel.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.4),
// constrain to top
rightLabel.topAnchor.constraint(equalTo: topAnchor),
// constrain to right
rightLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
// constrain width = 60%
rightLabel.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.6),
// constrain bottom of view (self) to >= 0 from bottom of leftLabel
bottomAnchor.constraint(greaterThanOrEqualTo: leftLabel.bottomAnchor, constant: 0.0),
// constrain bottom of view (self) to >= 0 from bottom of rightLabel
bottomAnchor.constraint(greaterThanOrEqualTo: rightLabel.bottomAnchor, constant: 0.0),
])
leftLabel.text = "Short string"
rightLabel.text = "Short string too"
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class MyViewController : UIViewController {
var theButton: UIButton = {
let b = UIButton()
b.setTitle("Tap Me", for: .normal)
b.translatesAutoresizingMaskIntoConstraints = false
b.backgroundColor = .red
return b
}()
var theStackView: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .vertical
v.spacing = 8
v.distribution = .equalSpacing
return v
}()
var myView = MyCustomView()
// on button tap, change the text in the label(s)
#objc func didTap(_ sender: Any?) -> Void {
myView.leftLabel.text = "Short string with\nA\nB\nC\nD\nE"
myView.rightLabel.text = "Short string too\nA\nB"
}
override func loadView() {
let view = UIView()
self.view = view
view.backgroundColor = .white
view.addSubview(theButton)
// constrain button to Top: 32 and centerX
NSLayoutConstraint.activate([
theButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 32.0),
theButton.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0.0),
])
view.addSubview(theStackView)
// constrain stack view to Top: 100 and Leading/Trailing" 0
// no Bottom or Height constraint
NSLayoutConstraint.activate([
theStackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 100.0),
theStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0.0),
theStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0.0),
])
theStackView.addArrangedSubview(myView)
// add an action for the button tap
theButton.addTarget(self, action: #selector(didTap(_:)), for: .touchUpInside)
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()