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.
I'm trying to create two views inside a main view and these view are autoresized by a textView, I want to add first one view and then by clicking a button add the seconds but I have some errors.
But I don't want to put equal height constraint to the main view, it has to resize according to textviews inside its 2 subviews
Here's a snippet of my code and an image of what i mean:
#objc func buttonAction() {
print("action")
mainView.addSubview(secondView)
firstView.topAnchor.constraint(equalTo: mainView.topAnchor).isActive = false
// I've tried to disable it to change the topAnchor of firstView
NSLayoutConstraint.activate([
secondView.bottomAnchor.constraint(equalTo: firstView.topAnchor),
secondView.leadingAnchor.constraint(equalTo: mainView.leadingAnchor),
secondView.trailingAnchor.constraint(equalTo: mainView.trailingAnchor),
secondView.topAnchor.constraint(equalTo: mainView.topAnchor)
])
}
func addConstraints() {
NSLayoutConstraint.activate([
firstView.topAnchor.constraint(equalTo: mainView.topAnchor),
firstView.bottomAnchor.constraint(equalTo: mainView.bottomAnchor),
firstView.leadingAnchor.constraint(equalTo: mainView.leadingAnchor),
firstView.trailingAnchor.constraint(equalTo: mainView.trailingAnchor),
])
NSLayoutConstraint.activate([
mainView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
mainView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
class View1: UIView { // the same as View2 class
lazy var textView: UITextView = {
let tv = UITextView()
tv.backgroundColor = .red
tv.isScrollEnabled = false
tv.font = UIFont(name: "Avenir", size: 16)
tv.text = "First"
tv.translatesAutoresizingMaskIntoConstraints = false
return tv
}()
init() {
super.init(frame: .zero)
setup()
}
required init?(coder: NSCoder) {
fatalError()
}
func setup() {
addSubview(textView)
textView.fillSuperview(padding: .init(top: 4, left: 4, bottom: 4, right: 4))
// fillSuperview() put 4 in top,left,right,bottom of superview
}
}
Here's an image of what I mean:
You need to save a reference to the firstView top constraint and deactivate that reference. You were trying to deactivate a new reference to the firstView top constraint.
class ViewController: UIViewController {
var firstViewTop: NSLayoutConstraint!
func addConstraints() {
firstViewTop = firstView.topAnchor.constraint(equalTo: mainView.topAnchor)
NSLayoutConstraint.activate([
firstViewTop,
firstView.bottomAnchor.constraint(equalTo: mainView.bottomAnchor),
firstView.leadingAnchor.constraint(equalTo: mainView.leadingAnchor),
firstView.trailingAnchor.constraint(equalTo: mainView.trailingAnchor),
])
NSLayoutConstraint.activate([
mainView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
mainView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
#objc func buttonAction() {
mainView.addSubview(secondView)
firstViewTop.isActive = false
NSLayoutConstraint.activate([
secondView.bottomAnchor.constraint(equalTo: firstView.topAnchor),
secondView.leadingAnchor.constraint(equalTo: mainView.leadingAnchor),
secondView.trailingAnchor.constraint(equalTo: mainView.trailingAnchor),
secondView.topAnchor.constraint(equalTo: mainView.topAnchor)
])
}
}
I have a UIScrollView on which I place some text fields and a tableView.
This is not a table delegate issue. I can get didSelectRowAt if I click and drag on a row. Just the simple row tap does not get through.
I do not show the table implementation below because it generally works and I think it is the gesture bit that is causing the problem.
I have added a gesture recognizer to the UIScrollView so that if you tap on the view background you will end the editing of the current field. This works to dismiss editing and keyboard, but for a UITableView I do not get the didSelectRowAt when I tap on a row. I have to press the row and drag horizontally in order to get the didSelectRowAt method call.
Is there a way to forward the tap gesture to the UITableView? Or is it just a bad idea to use the gesture recognizer on the UIScrollView if I have a table on that view? A button on that same view will get the tap and process the button event. Not sure why the table will not get the tap. The table will scroll also properly, but does not see the row tap.
The main UIView Class Methods
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(white: 1, alpha: 1.0) // was .97
// Container
setupScrollView()
registerKeyboardNotifications()
addGestureRecognizer()
}
func setupScrollView() {
view.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.contentSize = CGSize(width: view.frame.width, height: 1000)
scrollView.autoresizingMask = UIViewAutoresizing.flexibleBottomMargin
scrollView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
scrollView.widthAnchor.constraint(equalTo: view.widthAnchor, constant: 0).isActive = true
scrollView.heightAnchor.constraint(equalTo: view.heightAnchor, constant: 0).isActive = true
}
func addGestureRecognizer() {
// When tap on the container -- end editing on current field
let tapRecognizer = UITapGestureRecognizer(target: self,action: #selector(tapDidTouch(sender: )))
self.scrollView.addGestureRecognizer(tapRecognizer)
}
#objc func tapDidTouch(sender: Any) { // this method eats UITableView clicks
self.view.endEditing(true)
}
The extension for handling keyboard
extension TestView {
func registerKeyboardNotifications() {
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self,selector: #selector(keyboardWillShow(notif:)),name: .UIKeyboardWillShow,object: nil)
notificationCenter.addObserver( self, selector: #selector(keyboardWillHide(notif:)),name: .UIKeyboardWillHide,object: nil)
notificationCenter.addObserver(self, selector: #selector(keyboardWillShow(notif:)), name: .UIKeyboardWillChangeFrame, object: nil)
}
#objc func keyboardWillShow(notif: Notification) {
guard let frame = notif.userInfo?[UIKeyboardFrameEndUserInfoKey] as? CGRect else { return}
scrollView.contentInset = UIEdgeInsets(top: 0.0,
left: 0.0,
bottom: frame.height,
right: 0.0)
}
#objc func keyboardWillHide(notif: Notification) {
scrollView.contentInset = UIEdgeInsets()
}
}
I have a horizontal UIStackView that, by default, looks as follows:
The view with the heart is initially hidden and then shown at runtime. I would like to reduce the spacing between the heart view and the account name view.
The following code does the job, but only, when executed in viewDidLoad:
stackView.setCustomSpacing(8, after: heartView)
When changing the custom spacing later on, say on a button press, it doesn't have any effect. Now, the issue here is, that the custom spacing is lost, once the subviews inside the stack view change: when un-/hiding views from the stack view, the custom spacing is reset and cannot be modified.
Things, I've tried:
verified the spacing is set by printing stackView.customSpacing(after: heartView) (which properly returns 8)
unsuccessfully ran several reload functions:
stackView.layoutIfNeeded()
stackView.layoutSubviews()
view.layoutIfNeeded()
view.layoutSubviews()
viewDidLayoutSubviews()
How can I update the custom spacing of my stack view at runtime?
You need to make sure the UIStackView's distribution property is set to .fill or .fillProportionally.
I created the following swift playground and it looks like I am able to use setCustomSpacing at runtime with random values and see the effect of that.
import UIKit
import PlaygroundSupport
public class VC: UIViewController {
let view1 = UIView()
let view2 = UIView()
let view3 = UIView()
var stackView: UIStackView!
public init() {
super.init(nibName: nil, bundle: nil)
}
public required init?(coder aDecoder: NSCoder) {
fatalError()
}
public override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view1.backgroundColor = .red
view2.backgroundColor = .green
view3.backgroundColor = .blue
view2.isHidden = true
stackView = UIStackView(arrangedSubviews: [view1, view2, view3])
stackView.spacing = 10
stackView.axis = .horizontal
stackView.distribution = .fillProportionally
let uiSwitch = UISwitch()
uiSwitch.addTarget(self, action: #selector(onSwitch), for: .valueChanged)
view1.addSubview(uiSwitch)
uiSwitch.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
uiSwitch.centerXAnchor.constraint(equalTo: view1.centerXAnchor),
uiSwitch.centerYAnchor.constraint(equalTo: view1.centerYAnchor)
])
view.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stackView.heightAnchor.constraint(equalToConstant: 50),
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 50),
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -50)
])
}
#objc public func onSwitch(sender: Any) {
view2.isHidden = !view2.isHidden
if !view2.isHidden {
stackView.setCustomSpacing(CGFloat(arc4random_uniform(40)), after: view2)
}
}
}
PlaygroundPage.current.liveView = VC()
PlaygroundPage.current.needsIndefiniteExecution = true
Another reason setCustomSpacing can fail is if you call it before adding the arranged subview after which you want to apply the spacing.
Won't work:
headerStackView.setCustomSpacing(50, after: myLabel)
headerStackView.addArrangedSubview(myLabel)
Will work:
headerStackView.addArrangedSubview(myLabel)
headerStackView.setCustomSpacing(50, after: myLabel)
I also noticed that custom spacing values get reset after hiding/unhiding children. I was able to override updateConstraints() for my parent view and set the custom spacing as needed. The views then kept their intended spacing.
override func updateConstraints() {
super.updateConstraints()
stackView.setCustomSpacing(10, after: childView)
}
I have seen several questions like this but none of the answers have managed to fix it for me.
I have a view at the bottom of the screen that contains a scrollView that contains a stackView that will be populated with buttons.
My view is built programmatically like so:
import UIKit
class BottomBar: UIView {
typealias BindTap = ((String) -> Void)?
private let scrollView = UIScrollView()
private let buttonsStackView = UIStackView()
var onTap: BindTap
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setUpViews()
setUpLayout()
}
private func setUpViews() {
backgroundColor = .cyan
scrollView.backgroundColor = .red
buttonsStackView.backgroundColor = .green
buttonsStackView.alignment = .fill
buttonsStackView.distribution = .equalSpacing
buttonsStackView.axis = .horizontal
buttonsStackView.spacing = 5
scrollView.addSubview(buttonsStackView)
addSubview(scrollView)
}
private func setUpLayout() {
buttonsStackView.pinToSuperview(edges: [.top, .bottom, .left, .right],
constant: 5,
priority: .defaultHigh)
scrollView.pinToSuperview(edges: [.top, .bottom, .left, .right],
constant: 0,
priority: .defaultHigh)
}
func addModelButtons(models: [Model]) {
models.forEach { model in
let modelButton = UIButton()
modelButton.backgroundColor = .lightGray
modelButton.setTitle(model.fileName, for: .normal)
modelButton.addTarget(self, action: #selector(modelButtonTapped), for: .touchUpInside)
buttonsStackView.addArrangedSubview(modelButton)
if let first = models.first,
first.fileName == model.fileName {
updateSelectedButtonColor(modelButton)
}
}
}
#objc private func modelButtonTapped(button: UIButton) {
guard let modelName = button.titleLabel?.text else { return }
onTap?(modelName)
resetButtonColors()
updateSelectedButtonColor(button)
}
private func resetButtonColors() {
for case let button as UIButton in buttonsStackView.subviews {
button.backgroundColor = .lightGray
}
}
private func updateSelectedButtonColor(_ button: UIButton) {
button.backgroundColor = .darkGray
}
}
I cant see what is missing. I've added a picture so you can see that the stackView is wrapping around the buttons and not filling the scrollview.
Any help would be great. Im sure its a simple fix i just cant see it!
Here's the thing to understand about scrollViews. Unless you give the content area of a scrollView an explicit size, it will determine its size from its subviews.
In your case, you've told it that your stackView is 5 points away from the edges of the scrollView. That ties the size of the stackView to the size of the content area of the scrollView. At this point, the stackView is controlling the size of the scrollable area of the scrollView. Since your stackView only has 2 buttons, the stackView shrinks to the size of those two buttons and the scrollable area of the scrollView is 10 wider than that. Since the buttons are small, this does not fill the screen.
What you want is that the buttons stretch to fill the apparent size of the scrollView. In order for that to happen, you need to tell Auto Layout that the stackView's width must be greater than or equal to the width of the scrollView - 10.
Add this constraint after scrollView.addSubview(buttonsStackView):
buttonsStackView.widthAnchor.constraint(greaterThanOrEqualTo: scrollView.widthAnchor, multiplier: 1, constant: -10).isActive = true