I would like to arrange four buttons with Visual Format Language around the central X an Y of a view without hard coding any points, preferring to scale with constraints.
I can only achieve a cluster of buttons to align to the bottom margin, how do I centre them with the spacing you see (e.g. ~20 points) without resorting to NSLayoutConstraint?
I did not place them in a stack, they are all separate buttons.
I read that stacks were not a good idea, but it seems like the logical way, otherwise they stretch out vertically.
Ideally I would like to use VFL to make a calculator UI but am trying this first.
#IBDesignable class images_and_constraints: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
calcButtons()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
calcButtons()
}
private func calcButtons() {
let calcPlus = UIButton()
calcPlus.translatesAutoresizingMaskIntoConstraints = false
calcPlus.setTitle("+", for: .normal)
calcPlus.setTitleColor(UIColor.black, for: .normal)
calcPlus.setTitleColor(UIColor.white, for: .highlighted)
calcPlus.backgroundColor = UIColor.orange
addSubview(calcPlus)
let calcSubtract = UIButton()
calcSubtract.translatesAutoresizingMaskIntoConstraints = false
calcSubtract.setTitle("-", for: .normal)
calcSubtract.setTitleColor(UIColor.black, for: .normal)
calcSubtract.setTitleColor(UIColor.white, for: .highlighted)
calcSubtract.backgroundColor = UIColor.orange
addSubview(calcSubtract)
let calcMultiply = UIButton()
calcMultiply.translatesAutoresizingMaskIntoConstraints = false
calcMultiply.setTitle("x", for: .normal)
calcMultiply.setTitleColor(UIColor.black, for: .normal)
calcMultiply.setTitleColor(UIColor.white, for: .highlighted)
calcMultiply.backgroundColor = UIColor.orange
addSubview(calcMultiply)
let calcDivide = UIButton()
calcDivide.translatesAutoresizingMaskIntoConstraints = false
calcDivide.setTitle("/", for: .normal)
calcDivide.setTitleColor(UIColor.black, for: .normal)
calcDivide.setTitleColor(UIColor.white, for: .highlighted)
calcDivide.backgroundColor = UIColor.orange
addSubview(calcDivide)
let views = ["calcPlus": calcPlus,
"calcSubtract": calcSubtract,
"calcMultiply": calcMultiply,
"calcDivide": calcDivide]
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|-[calcPlus]-[calcSubtract(==calcPlus)]-|",
options: .alignAllBottom,
metrics: nil,
views: views))
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|-[calcMultiply]-[calcDivide(==calcMultiply)]-|",
options: .alignAllTop,
metrics: nil,
views: views))
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "V:[calcSubtract]-[calcDivide(==calcSubtract)]-|",
options: .alignAllCenterX,
metrics: nil,
views: views))
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:[calcSubtract]",
options: .alignAllCenterX,
metrics: nil,
views: views))
}
}
Using VFL to center views requires trickery.
Look at this question and particularly this answer for the trick.
For the kind of layout you want, VFL is just not a good fit.
Just one NSLayoutConstraint in addition to VFL would solve it but since you're only interested in VFL, I would suggest you use the trick to center a container view that holds your buttons.
Solution:
func calcButtons() {
//1. Create a container view that will contain your operator buttons
let buttonContainerView = UIView()
buttonContainerView.backgroundColor = UIColor.lightGray
buttonContainerView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(buttonContainerView)
//Place it vertically in the center of the superview
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "V:[superview]-(<=1)-[childView]",
options: .alignAllCenterX,
metrics: nil,
views: ["superview" : self,
"childView" : buttonContainerView]))
//Place it horizontally in the center of the superview + equal widths to superview
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:[superview]-(<=1)-[childView(==superview)]",
options: .alignAllCenterY,
metrics: nil,
views: ["superview" : self,
"childView" : buttonContainerView]))
//2. Create your buttons as you were:
//DRY Fix: Helper function to create button and add it to `buttonContainerView`
func addButton(title: String, selector: Selector? = nil) -> UIButton {
let button = UIButton()
button.backgroundColor = UIColor.orange
button.setTitle(title, for: .normal)
button.setTitleColor(UIColor.black, for: .normal)
button.setTitleColor(UIColor.white, for: .highlighted)
//You might need this later cuz a button gotta do wat a button gotta do
if let selector = selector {
button.addTarget(self, action: selector, for: UIControlEvents.touchUpInside)
}
button.translatesAutoresizingMaskIntoConstraints = false
buttonContainerView.addSubview(button)
return button
}
let calcPlus = addButton(title: "+", selector: #selector(CalculatorView.add))
let calcSubtract = addButton(title: "-")
let calcMultiply = addButton(title: "x")
let calcDivide = addButton(title: "/")
let views = ["calcPlus": calcPlus,
"calcSubtract": calcSubtract,
"calcMultiply": calcMultiply,
"calcDivide": calcDivide]
//Same as before
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|-[calcPlus]-[calcSubtract(==calcPlus)]-|",
options: .alignAllBottom,
metrics: nil,
views: views))
//Same as before
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|-[calcMultiply]-[calcDivide(==calcMultiply)]-|",
options: .alignAllTop,
metrics: nil,
views: views))
/*
Same as before but this time we give a top constraint too
i.e.
"V:|-[calcSubtract]..."
instead of
"V:[calcSubtract]..."
*/
//
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "V:|-[calcSubtract]-[calcDivide(==calcSubtract)]-|",
options: .alignAllCenterX,
metrics: nil,
views: views))
}
In the end I decided on NSLayoutConstraint.activate of which each button would be reliant on the one before it (rows), with the leading (far left for left-to-right readers) button constrained to the one above it.
calculatriceButtons["7"]!.leadingAnchor.constraint(equalTo: guide.leadingAnchor, constant: 1.0),
calculatriceButtons["7"]!.topAnchor.constraint(equalTo: calculatriceButtons["C"]!.bottomAnchor, constant: 1.0),
This was the best way to assure the buttons scaled on all devices.
There is a new alternative to using VFL which is what I use in code now.
Layout Anchors
Each view has different anchors. leading, trailing, top, bottom, etc...
You can use these to create constraints for you...
NSLayoutConstraint.activate([
viewB.leadingAnchor.constraint(equalTo: viewA.leadingAnchor, constant: 20),
viewA.widthAnchor.constraint(equalTo: viewB.widthAnchor)
])
for example.
Stack View
In addition to that there is an even more modern approach which is to use UIStackView. This is a really useful view that takes away the need to add constraints and does it for you.
let stackView = UIStackView(arrangedSubViews: [viewA, viewB])
stackView.spacing = 20
stackView.axis = .horizontal
stackView.alignment = .center
stackView.distribution = .fillEqually
You can also nest stack views to create more complex layouts.
Definitely worth looking in to...
https://developer.apple.com/documentation/uikit/uistackview?changes=_6
Creating your layout
let upperStackView = UIStackView(arrangedSubviews: [topLeft, topRight])
upperStackView.axis = .horizontal
upperStackView.distribution = .fillEqually
upperStackView.spacing = 20
let lowerStackView = UIStackView(arrangedSubviews: [bottomLeft, bottomRight])
lowerStackView.axis = .horizontal
lowerStackView.distribution = .fillEqually
lowerStackView.spacing = 20
let mainStackView = UIStackView(arrangedSubviews: [upperStackView, lowerStackView])
mainStackView.axis = .vertical
mainStackView.distribution = .fillEqually
mainStackView.spacing = 20
view.addSubview(mainStackView)
NSLayoutConstraint.activate([
mainStackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
mainStackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
mainStackView.widthAnchor.constraint(equalToConstant: 200),
mainStackView.heightAnchor.constraint(equalToConstant: 200),
])
Why not VFL?
While VFL was a nice first attempt at AutoLayout, I feel that Apple has moved away from it now and are moving towards these more succinct methods of creating AutoLayout constraints.
It still allows you to think in constraints while writing code but provides a slightly more modern approach.
Of course... you can also create UIStackView in Interface Builder also :D
Related
I have a simple iOS App, containing a Scroll View (scroll horizontally, but mainly as described here: Is it possible for UIStackView to scroll?). By default the Horizontal Stack is empty (spacing = Standard, Distribution is Fill, but also Equal Spacing does not improve this), and I add buttons programatically to it (for example purpose I add a random length string, to have various sizes), using:
filterStackView.translatesAutoresizingMaskIntoConstraints = false
let newButtonConfiguration = UIButton.Configuration.filled()
let newButton = UIButton(configuration: newButtonConfiguration)
newButton.translatesAutoresizingMaskIntoConstraints = false
newButton.setTitle("\(randomString(length: Int.random(in: 5..<25)))", for: .normal)
newButton.setImage(UIImage(systemName: "xmark.circle", withConfiguration: UIImage.SymbolConfiguration(scale: .unspecified)), for: .normal)
newButton.addTarget(self, action: #selector(newButtonPressedAction), for: .touchUpInside)
filterStackView.addArrangedSubview(newButton)
I already tried adding to the button:
newButton.sizeToFit()
newButton.layoutIfNeeded()
or to the View Stack:
filterStackView.sizeToFit()
filterStackView.layoutIfNeeded()
Without any visible change. The weird things happening are:
After adding another one, sizes start changing in a funny way (if buttons are added from Story Board they really respect the width of the text - and Autolayout works fine):
And after playing with adding and removing them for a while:
And in the debug console I see constraints failing badly:
2022-11-01 14:05:48.450435+0100 TestAppSideScroll[20626:640959] [LayoutConstraints] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(
"<NSLayoutConstraint:0x600001b58c80 'UISV-canvas-connection' H:[UIButton:0x122408660'N6xoAsl']-(0)-| (active, names: '|':UIStackView:0x1505080c0 )>",
"<_UISystemBaselineConstraint:0x600001b586e0 'UISV-spacing' H:[UIButton:0x1223095d0'oHSryjPsjrZA']-(NSLayoutAnchorConstraintSpace(8))-[UIButton:0x122408660'N6xoAsl'] (active)>"
)
Will attempt to recover by breaking constraint
<_UISystemBaselineConstraint:0x600001b586e0 'UISV-spacing' H:[UIButton:0x1223095d0'oHSryjPsjrZA']-(NSLayoutAnchorConstraintSpace(8))-[UIButton:0x122408660'N6xoAsl'] (active)>
Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful.
Which actually makes no sense, since the conflict seems to be the spacing constraint with the connection to the canvas constraint? But that I would expect to happen for every single button I add, if that would be the general problem - however there are no real constraints, except the spacing and the snapping of the scroll view.
Any help would be appreciated.
Yeah... something really weird going on there.
My guess would be that UIKit has some sort of algorithm that is trying to apply "readability" to the buttons. Clearly, though, that's not the goal here.
After some quick testing, one way to get around this is to add the button as a subview to a clear "container" view, and then add that container view to the stack view.
Some sample code:
class SampleViewController: UIViewController {
let filterStackViewA: UIStackView = {
let v = UIStackView()
v.spacing = 8
v.distribution = .fill
v.alignment = .center
return v
}()
let filterStackViewB: UIStackView = {
let v = UIStackView()
v.spacing = 8
v.distribution = .fill
v.alignment = .center
return v
}()
let scrollViewA: UIScrollView = {
let v = UIScrollView()
v.backgroundColor = .systemYellow
return v
}()
let scrollViewB: UIScrollView = {
let v = UIScrollView()
v.backgroundColor = .systemOrange
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .darkGray
let addBtn = UIButton()
addBtn.setTitle("Add Button", for: [])
addBtn.setTitleColor(.white, for: .normal)
addBtn.setTitleColor(.lightGray, for: .highlighted)
addBtn.backgroundColor = .systemGreen
addBtn.layer.cornerRadius = 8
let labelA: UILabel = {
let v = UILabel()
v.textColor = .white
v.font = .systemFont(ofSize: 15.0, weight: .light)
v.text = "Buttons added to \"Container\" views..."
return v
}()
let labelB: UILabel = {
let v = UILabel()
v.textColor = .white
v.font = .systemFont(ofSize: 15.0, weight: .light)
v.text = "Buttons added directly to stack view..."
return v
}()
[addBtn, scrollViewA, scrollViewB, filterStackViewA, filterStackViewB, labelA, labelB].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
view.addSubview(addBtn)
view.addSubview(labelA)
view.addSubview(scrollViewA)
scrollViewA.addSubview(filterStackViewA)
view.addSubview(labelB)
view.addSubview(scrollViewB)
scrollViewB.addSubview(filterStackViewB)
let g = view.safeAreaLayoutGuide
let cgA = scrollViewA.contentLayoutGuide
let fgA = scrollViewA.frameLayoutGuide
let cgB = scrollViewB.contentLayoutGuide
let fgB = scrollViewB.frameLayoutGuide
NSLayoutConstraint.activate([
addBtn.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
addBtn.widthAnchor.constraint(equalToConstant: 200.0),
addBtn.centerXAnchor.constraint(equalTo: g.centerXAnchor),
labelA.topAnchor.constraint(equalTo: addBtn.bottomAnchor, constant: 40.0),
labelA.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
labelA.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollViewA.topAnchor.constraint(equalTo: labelA.bottomAnchor, constant: 8.0),
scrollViewA.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
scrollViewA.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollViewA.heightAnchor.constraint(equalToConstant: 60.0),
filterStackViewA.topAnchor.constraint(equalTo: cgA.topAnchor, constant: 4.0),
filterStackViewA.leadingAnchor.constraint(equalTo: cgA.leadingAnchor, constant: 8.0),
filterStackViewA.trailingAnchor.constraint(equalTo: cgA.trailingAnchor, constant: -8.0),
filterStackViewA.bottomAnchor.constraint(equalTo: cgA.bottomAnchor, constant: -4.0),
filterStackViewA.heightAnchor.constraint(equalTo: fgA.heightAnchor, constant: -8.0),
labelB.topAnchor.constraint(equalTo: filterStackViewA.bottomAnchor, constant: 20.0),
labelB.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
labelB.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollViewB.topAnchor.constraint(equalTo: labelB.bottomAnchor, constant: 8.0),
scrollViewB.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
scrollViewB.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollViewB.heightAnchor.constraint(equalToConstant: 60.0),
filterStackViewB.topAnchor.constraint(equalTo: cgB.topAnchor, constant: 4.0),
filterStackViewB.leadingAnchor.constraint(equalTo: cgB.leadingAnchor, constant: 8.0),
filterStackViewB.trailingAnchor.constraint(equalTo: cgB.trailingAnchor, constant: -8.0),
filterStackViewB.bottomAnchor.constraint(equalTo: cgB.bottomAnchor, constant: -4.0),
filterStackViewB.heightAnchor.constraint(equalTo: fgB.heightAnchor, constant: -8.0),
])
addBtn.addTarget(self, action: #selector(addButton), for: .touchUpInside)
}
#objc func addButton() {
// unwrap optional system image
guard let img = UIImage(systemName: "xmark.circle", withConfiguration: UIImage.SymbolConfiguration(scale: .unspecified))
else { return }
var newButtonConfiguration = UIButton.Configuration.filled()
newButtonConfiguration.title = randomString(length: Int.random(in: 5..<15))
newButtonConfiguration.image = img
// let's add a new button to a "container" view
let newButtonA = UIButton(configuration: newButtonConfiguration)
newButtonA.translatesAutoresizingMaskIntoConstraints = false
let v = UIView()
v.addSubview(newButtonA)
NSLayoutConstraint.activate([
newButtonA.topAnchor.constraint(equalTo: v.topAnchor),
newButtonA.leadingAnchor.constraint(equalTo: v.leadingAnchor),
newButtonA.trailingAnchor.constraint(equalTo: v.trailingAnchor),
newButtonA.bottomAnchor.constraint(equalTo: v.bottomAnchor),
])
// add the container view to the stack view
filterStackViewA.addArrangedSubview(v)
// let's add a new button directly to the stack view
let newButtonB = UIButton(configuration: newButtonConfiguration)
filterStackViewB.addArrangedSubview(newButtonB)
newButtonA.addTarget(self, action: #selector(removeMe(_:)), for: .touchUpInside)
newButtonB.addTarget(self, action: #selector(removeMe(_:)), for: .touchUpInside)
// let's make sure the new button is visible
// note: this is just for example...
// if we rapidly tap and add buttons, this can easily "miss" the last button
// so don't expect this to be "production" code
DispatchQueue.main.async {
let sz = self.scrollViewA.contentSize
let rA = CGRect(x: sz.width - 1.0, y: 0.0, width: 1.0, height: 1.0)
self.scrollViewA.scrollRectToVisible(rA, animated: true)
let szB = self.scrollViewB.contentSize
let rB = CGRect(x: szB.width - 1.0, y: 0.0, width: 1.0, height: 1.0)
self.scrollViewB.scrollRectToVisible(rB, animated: true)
}
}
#objc func removeMe(_ sender: UIButton) {
// get a reference to the tapped button's superview
guard let sv = sender.superview else { return }
var v: UIView!
if sv is UIStackView {
// we tapped a button added directly to the stack view
v = sender
} else {
// we tapped a button that's in a "container" view
v = sv
}
guard let st = v.superview as? UIStackView,
let idx = st.arrangedSubviews.firstIndex(of: v),
filterStackViewA.arrangedSubviews.count > idx,
filterStackViewB.arrangedSubviews.count > idx
else {
print("something's not setup right, so return")
return
}
let bA = filterStackViewA.arrangedSubviews[idx]
let bB = filterStackViewB.arrangedSubviews[idx]
// let's animate the buttons away
bA.isHidden = true
bB.isHidden = true
UIView.animate(withDuration: 0.5, animations: {
self.view.layoutIfNeeded()
}, completion: { _ in
bA.removeFromSuperview()
bB.removeFromSuperview()
})
}
func randomString(length: Int) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String((0..<length).map{ _ in letters.randomElement()! })
}
}
It will look like this when running:
Each tap of "Add Button" will add a new button to each scrolling-stackView, with the top set using buttons in container views. Tapping any of those buttons will remove it from its stack view, along with its "twin" from the other one.
I want to create a chat bottom bar with the following content, in a horizontal UIStackView:
a UITextField on the left side, taking all the available space
a label on the right with fixed size (default intrinsic content size)
Here is my code:
let messageTextField = UITextField()
messageTextField.translatesAutoresizingMaskIntoConstraints = false
messageTextField.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
messageTextField.setContentHuggingPriority(.defaultHigh, for: .horizontal)
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "SEND"
label.setContentHuggingPriority(.defaultLow, for: .horizontal)
label.setContentCompressionResistancePriority(.required, for: .horizontal)
label.textColor = .red
let sv = UIStackView()
sv.axis = .horizontal
sv.distribution = .fill
sv.alignment = .center
sv.translatesAutoresizingMaskIntoConstraints = false
// constraint the stack view to the bottomBar view
bottomBar.addSubview(sv)
NSLayoutConstraint.activate([
sv.leadingAnchor.constraint(equalTo: bottomBar.leadingAnchor, constant: 20),
sv.trailingAnchor.constraint(equalTo: bottomBar.trailingAnchor, constant: -20),
sv.bottomAnchor.constraint(equalTo: bottomBar.bottomAnchor, constant: -8),
sv.topAnchor.constraint(equalTo: bottomBar.topAnchor, constant: 8)
])
sv.addArrangedSubview(messageTextField)
sv.addArrangedSubview(label)
NSLayoutConstraint.activate([
messageTextField.heightAnchor.constraint(equalTo:sv.heightAnchor),
// I didn't put any width constraint for textField because it make no sense, as I want it to take all the available space on the right
])
With this code, the label is expanding its size and filling all the available space on the right, and the text field is not showing (inspector says width = 0); exactly the opposite of what I want.
I don't understand why it doesn't work because I explicitly configured it so it does not expand the label with:
label.setContentHuggingPriority(.defaultLow, for: .horizontal)
and instead to expand the text field with:
messageTextField.setContentHuggingPriority(.defaultHigh, for: .horizontal)
What did I miss here? I have read through many SO answers, Apple documentation, and followed various tutorials and videos, but I'm not able to achieve this seemingly simple thing.
You have it backwards...
Setting Content Hugging Priority means" control how this element hugs its content.
So, as an example, if you have a UILabel with only the letter "A" in it, a High content hugging priority will keep the label only as wide as that single letter.
So with these two lines:
label.setContentHuggingPriority(.defaultLow, for: .horizontal)
messageTextField.setContentHuggingPriority(.defaultHigh, for: .horizontal)
you've told auto-layout:
keep messageTextField only as wide as its text, and allow label to stretch wider than its text
What you want is:
// don't let label stretch at all
label.setContentHuggingPriority(.required, for: .horizontal)
// let message text field stretch if needed
messageTextField.setContentHuggingPriority(.defaultHigh, for: .horizontal)
The stack view will then stretch the text field to fill the available space.
I tried quickly on Xcode 12.3 on Simulator (iPhone 12 )
and leaving the default values to the stackview and the content hugging, worked for me:
override func viewDidLoad() {
super.viewDidLoad()
// Just added a view anchored to the bottom of the view controller for the purpose of the test
let bottomBar = UIView()
bottomBar.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(bottomBar)
bottomBar.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
bottomBar.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
bottomBar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
bottomBar.heightAnchor.constraint(equalToConstant: 40.0).isActive = true
bottomBar.backgroundColor = .yellow
let messageTextField = UITextField()
messageTextField.translatesAutoresizingMaskIntoConstraints = false
messageTextField.backgroundColor = .cyan
// LEAVING DEFAULTS
//messageTextField.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
//messageTextField.setContentHuggingPriority(.defaultHigh, for: .horizontal)
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "SEND"
// LEAVING DEFAULTS
//label.setContentHuggingPriority(.defaultLow, for: .horizontal)
//label.setContentCompressionResistancePriority(.required, for: .horizontal)
label.textColor = .red
let sv = UIStackView()
sv.axis = .horizontal
// LEAVING DEFAULTS
//sv.distribution = .fill
//sv.alignment = .center
sv.translatesAutoresizingMaskIntoConstraints = false
// constraint the stack view to the bottomBar view
bottomBar.addSubview(sv)
NSLayoutConstraint.activate([
sv.leadingAnchor.constraint(equalTo: bottomBar.leadingAnchor, constant: 20),
sv.trailingAnchor.constraint(equalTo: bottomBar.trailingAnchor, constant: -20),
sv.bottomAnchor.constraint(equalTo: bottomBar.bottomAnchor, constant: -8),
sv.topAnchor.constraint(equalTo: bottomBar.topAnchor, constant: 8)
])
sv.addArrangedSubview(messageTextField)
sv.addArrangedSubview(label)
NSLayoutConstraint.activate([
messageTextField.heightAnchor.constraint(equalTo:sv.heightAnchor),
])
}
This is the outcome:
what is wrong with my StackView?
This is the code:
class PushUpViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setUpStackView()
}
func setUpStackView() {
// SetUp StackView:
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.alignment = .center
stackView.distribution = .fillProportionally
stackView.spacing = 50
view.addSubview(stackView)
// SetUp StackView Constraints:
stackView.pin(to: view)
stackView.setCustomSpacing(50, after: PushUpButton)
stackView.setCustomSpacing(100, after: TimeLabel)
// Set Elements to StackView:
stackView.addArrangedSubview(TimeLabel)
stackView.addArrangedSubview(PushUpButton)
stackView.addArrangedSubview(secondStackView)
// SetUp PushUpButton:
PushUpButton.backgroundColor = .white
PushUpButton.setTitle("\(count)", for: .normal)
PushUpButton.setTitleColor(.systemGray, for: .normal)
PushUpButton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 70)
PushUpButton.translatesAutoresizingMaskIntoConstraints = false
PushUpButton.heightAnchor.constraint(equalToConstant: 300).isActive = true
PushUpButton.widthAnchor.constraint(equalToConstant: 150).isActive = true
// SetUp TimeLabel
TimeLabel.textAlignment = .center
TimeLabel.text = "\(counter)"
TimeLabel.textColor = .black
TimeLabel.font = .boldSystemFont(ofSize: 30)
self.view.addSubview(TimeLabel)
TimeLabel.translatesAutoresizingMaskIntoConstraints = false
TimeLabel.widthAnchor.constraint(equalToConstant: 300).isActive = true
TimeLabel.heightAnchor.constraint(equalToConstant: 200).isActive = true
// SetUp SecondStackView
secondStackView.translatesAutoresizingMaskIntoConstraints = false
secondStackView.axis = .horizontal
secondStackView.alignment = .center
secondStackView.distribution = .fillEqually
secondStackView.spacing = 20
// SetUp SecondStackView Constrains
secondStackView.heightAnchor.constraint(equalToConstant: 50).isActive = true
secondStackView.widthAnchor.constraint(equalTo: view.widthAnchor, constant: -15).isActive = true
// Set Elements:
secondStackView.addArrangedSubview(breakButton)
secondStackView.addArrangedSubview(stopbutton)
//SetUp BreakButton
breakButton.backgroundColor = .lightGray
breakButton.setTitle("Break", for: .normal)
breakButton.setTitle("Start", for: .selected)
breakButton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 20)
breakButton.setTitleColor(.white, for: .normal)
breakButton.layer.cornerRadius = 12
breakButton.layer.borderWidth = 1
breakButton.layer.borderColor = UIColor.white.cgColor
// breakButton.addTarget(self, action: #selector(BreakButtonTapped), for: .touchUpInside)
breakButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
breakButton.widthAnchor.constraint(equalToConstant: 150),
breakButton.heightAnchor.constraint(equalToConstant: 50)
])
// SetUp StopButton:
stopbutton.backgroundColor = .systemRed
stopbutton.setTitle("Stop", for: .normal)
stopbutton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 20)
stopbutton.setTitleColor(.white, for: .normal)
stopbutton.layer.cornerRadius = 12
stopbutton.layer.borderWidth = 1
stopbutton.layer.borderColor = UIColor.white.cgColor
// stopbutton.addTarget(self, action: #selector(stopButtonTapped), for: .touchUpInside)
stopbutton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stopbutton.widthAnchor.constraint(equalToConstant: 150),
stopbutton.heightAnchor.constraint(equalToConstant: 50)
])
}
}
And this is how it look:
But it should looks like this:
This is what comes in the console when I am on the StackView VC:
I have no idea what this means or what I should do to solve this problem
I do not understand StackViews... I watched a lot of yt tutorials but they are all the same and did't help me. My biggest problem is the distribution of the StackView: I don't know where the difference is
First tip: forget using .fillProportionally with stack views. It is almost never used ... and when it is used, it's used for very specific reasons.
Second tip: during development, give your UI elements contrasting background colors to make it easy to see the frames at run-time.
Third tip: Use leadingLowerCase for variable and function names... Use LeadingUpperCase for class names.
Fourth tip: group similar code together - such as setting view properties, setting constraints, etc - and include logical comments to make it easier to follow what your code is doing.
Take a look at this:
class PushUpViewController: UIViewController {
let stackView = UIStackView()
let secondStackView = UIStackView()
let pushUpButton = UIButton()
let breakButton = UIButton()
let stopbutton = UIButton()
let timeLabel = UILabel()
var count: Int = 0
var counter: Float = 0.0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setUpStackView()
}
func setUpStackView() {
// SetUp StackView:
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.alignment = .fill
stackView.distribution = .fill
// SetUp timeLabel
timeLabel.textAlignment = .center
timeLabel.text = "\(counter)"
timeLabel.textColor = .black
timeLabel.font = .boldSystemFont(ofSize: 30)
// SetUp pushUpButton:
pushUpButton.backgroundColor = .white
pushUpButton.setTitle("\(count)", for: .normal)
pushUpButton.setTitleColor(.systemGray, for: .normal)
pushUpButton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 70)
// SetUp secondStackView
secondStackView.axis = .horizontal
secondStackView.alignment = .fill
secondStackView.distribution = .fillEqually
secondStackView.spacing = 20
//SetUp breakButton
breakButton.backgroundColor = .lightGray
breakButton.setTitle("Break", for: .normal)
breakButton.setTitle("Start", for: .selected)
breakButton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 20)
breakButton.setTitleColor(.white, for: .normal)
breakButton.layer.cornerRadius = 12
breakButton.layer.borderWidth = 1
breakButton.layer.borderColor = UIColor.white.cgColor
// SetUp stopButton:
stopbutton.backgroundColor = .systemRed
stopbutton.setTitle("Stop", for: .normal)
stopbutton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 20)
stopbutton.setTitleColor(.white, for: .normal)
stopbutton.layer.cornerRadius = 12
stopbutton.layer.borderWidth = 1
stopbutton.layer.borderColor = UIColor.white.cgColor
// add buttons to horizontal second stack view
secondStackView.addArrangedSubview(breakButton)
secondStackView.addArrangedSubview(stopbutton)
// if we want the center PushUpButton to be 300 x 150
// and centered vertically
// we need to embed it in a clear view
let holderView = UIView()
// add PushUpButton to holderView
holderView.addSubview(pushUpButton)
// views added as arrangedSubviews of a stack view automatically get
// .translatesAutoresizingMaskIntoConstraints = false
// but, because we're adding the PushUpButton as a subview
// of holderView, we need to set it here
pushUpButton.translatesAutoresizingMaskIntoConstraints = false
// add views to stack view
stackView.addArrangedSubview(timeLabel)
stackView.addArrangedSubview(holderView)
stackView.addArrangedSubview(secondStackView)
// add stackView to view
view.addSubview(stackView)
// SetUp StackView Constraints:
//stackView.pin(to: view)
// respect safe-area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain stackview to full view (safe-area)
// to bottom with Zero extra space
stackView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
// to top with 20-pts "padding"
stackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
// and 8-pts "padding" on each side
stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
// pushUpButton should be 300x150
pushUpButton.widthAnchor.constraint(equalToConstant: 300.0),
pushUpButton.heightAnchor.constraint(equalToConstant: 150.0),
// pushUpButton should be centered in holderView
pushUpButton.centerXAnchor.constraint(equalTo: holderView.centerXAnchor),
pushUpButton.centerYAnchor.constraint(equalTo: holderView.centerYAnchor),
// bottom buttons should have Height: 50
secondStackView.heightAnchor.constraint(equalToConstant: 50.0),
])
// break and stop button actions
//breakButton.addTarget(self, action: #selector(BreakButtonTapped), for: .touchUpInside)
//stopbutton.addTarget(self, action: #selector(stopButtonTapped), for: .touchUpInside)
// during development, so we can see the layout easily
//holderView.backgroundColor = .yellow
//PushUpButton.backgroundColor = .green
//TimeLabel.backgroundColor = .cyan
}
}
Result on iPhone 11:
on iPhone 8:
and with background colors to help during development:
Additional Tip:
When learning auto layout (particularly stack views), work on your layout in Storyboard / Interface Builder. You can immediately see how it looks and what happens when changing values / properties. You can also change the View as: to immediately see how it looks on different devices / screen sizes. If you want to keep everything in code, once you have your layout looking the way you want, then replicate those constraints and settings in code.
I'm trying to implement a programmatic version of the Dynamic Stack View in Apple's Auto Layout Cookbook. An "Add Item" button is supposed to add new views to a vertical stackView, including a delete button to remove each view. My programmatic code works fine for 1 touch of the "Add Item" button, but then that button becomes inactive. As a result, I can only add 1 item to the stackView. If I used the delete button, the "Add Item" becomes active again. I've included an animated gif to illustrate.
I'm posting both my Programmatic code (which has the problem) and below that the original storyboard-based code (which works fine). I've tried putting a debug breakpoint at the addEntry func, but that didn't help. -Thanks
Programmatic Code ("Add Item" button only works once):
import UIKit
class CodeDynamStackVC: UIViewController {
// MARK: Properties
var scrollView = UIScrollView()
var stackView = UIStackView()
var button = UIButton()
// MARK: UIViewController
override func viewDidLoad() {
super.viewDidLoad()
// Set up the scrollview
let insets = UIEdgeInsets(top: 20, left: 0.0, bottom: 0.0, right: 0.0)
scrollView.contentInset = insets
scrollView.scrollIndicatorInsets = insets
setupInitialVertStackView()
}
//setup initial button inside vertical stackView
func setupInitialVertStackView() {
// make inital "Add Item" button
button = UIButton(type: .system)
button.setTitle("Add Item", for: .normal)
button.setTitleColor(UIColor.blue, for: .normal)
button.addTarget(self, action: #selector(addEntry), for: .touchUpInside)
//enclose button in a vertical stackView
stackView.addArrangedSubview(button)
stackView.axis = .vertical
stackView.alignment = .fill
stackView.distribution = .equalSpacing
stackView.spacing = 5
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
let viewsDictionary = ["v0":stackView]
let stackView_H = NSLayoutConstraint.constraints(withVisualFormat: "H:|[v0]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewsDictionary)
let stackView_V = NSLayoutConstraint.constraints(withVisualFormat: "V:|-20-[v0(25)]|", options: NSLayoutFormatOptions(rawValue:0), metrics: nil, views: viewsDictionary)
view.addConstraints(stackView_H)
view.addConstraints(stackView_V)
}
// MARK: Interface Builder actions
func addEntry() {
guard let addButtonContainerView = stackView.arrangedSubviews.last else { fatalError("Expected at least one arranged view in the stack view.") }
let nextEntryIndex = stackView.arrangedSubviews.count - 1
let offset = CGPoint(x: scrollView.contentOffset.x, y: scrollView.contentOffset.y + addButtonContainerView.bounds.size.height)
let newEntryView = createEntryView()
newEntryView.isHidden = true
stackView.insertArrangedSubview(newEntryView, at: nextEntryIndex)
UIView.animate(withDuration: 0.25, animations: {
newEntryView.isHidden = false
self.scrollView.contentOffset = offset
})
}
func deleteStackView(_ sender: UIButton) {
guard let entryView = sender.superview else { return }
UIView.animate(withDuration: 0.25, animations: {
entryView.isHidden = true
}, completion: { _ in
entryView.removeFromSuperview()
})
}
// MARK: Convenience
/// Creates a horizontal stackView entry to place within the parent vertical stackView
fileprivate func createEntryView() -> UIView {
let date = DateFormatter.localizedString(from: Date(), dateStyle: .short, timeStyle: .none)
let number = UUID().uuidString
let stack = UIStackView()
stack.axis = .horizontal
stack.alignment = .center
stack.distribution = .fill
stack.spacing = 8
let dateLabel = UILabel()
dateLabel.text = date
dateLabel.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.body)
let numberLabel = UILabel()
numberLabel.text = number
numberLabel.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.caption2)
numberLabel.setContentHuggingPriority(UILayoutPriorityDefaultLow - 1.0, for: .horizontal)
numberLabel.setContentCompressionResistancePriority(UILayoutPriorityDefaultHigh - 1.0, for: .horizontal)
let deleteButton = UIButton(type: .roundedRect)
deleteButton.setTitle("Del", for: UIControlState())
deleteButton.addTarget(self, action: #selector(DynamStackVC.deleteStackView(_:)), for: .touchUpInside)
stack.addArrangedSubview(dateLabel)
stack.addArrangedSubview(numberLabel)
stack.addArrangedSubview(deleteButton)
return stack
}
}
Storyboard-based Code ("Add Item" button always works)
import UIKit
class DynamStackVC: UIViewController {
// MARK: Properties
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var stackView: UIStackView!
// MARK: UIViewController
override func viewDidLoad() {
super.viewDidLoad()
// Set up the scrollview.
let insets = UIEdgeInsets(top: 20, left: 0.0, bottom: 0.0, right: 0.0)
scrollView.contentInset = insets
scrollView.scrollIndicatorInsets = insets
}
// MARK: Interface Builder actions
#IBAction func addEntry(_: AnyObject) {
guard let addButtonContainerView = stackView.arrangedSubviews.last else { fatalError("Expected at least one arranged view in the stack view.") }
let nextEntryIndex = stackView.arrangedSubviews.count - 1
let offset = CGPoint(x: scrollView.contentOffset.x, y: scrollView.contentOffset.y + addButtonContainerView.bounds.size.height)
let newEntryView = createEntryView()
newEntryView.isHidden = true
stackView.insertArrangedSubview(newEntryView, at: nextEntryIndex)
UIView.animate(withDuration: 0.25, animations: {
newEntryView.isHidden = false
self.scrollView.contentOffset = offset
})
}
func deleteStackView(_ sender: UIButton) {
guard let entryView = sender.superview else { return }
UIView.animate(withDuration: 0.25, animations: {
entryView.isHidden = true
}, completion: { _ in
entryView.removeFromSuperview()
})
}
// MARK: Convenience
/// Creates a horizontal stack view entry to place within the parent `stackView`.
fileprivate func createEntryView() -> UIView {
let date = DateFormatter.localizedString(from: Date(), dateStyle: .short, timeStyle: .none)
let number = UUID().uuidString
let stack = UIStackView()
stack.axis = .horizontal
stack.alignment = .center
stack.distribution = .fillProportionally
stack.spacing = 8
let dateLabel = UILabel()
dateLabel.text = date
dateLabel.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.body)
let numberLabel = UILabel()
numberLabel.text = number
numberLabel.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.caption2)
numberLabel.setContentHuggingPriority(UILayoutPriorityDefaultLow - 1.0, for: .horizontal)
numberLabel.setContentCompressionResistancePriority(UILayoutPriorityDefaultHigh - 1.0, for: .horizontal)
let deleteButton = UIButton(type: .roundedRect)
deleteButton.setTitle("Del", for: UIControlState())
deleteButton.addTarget(self, action: #selector(DynamStackVC.deleteStackView(_:)), for: .touchUpInside)
stack.addArrangedSubview(dateLabel)
stack.addArrangedSubview(numberLabel)
stack.addArrangedSubview(deleteButton)
return stack
}
}
I figured it out by putting background colors on all buttons and labels within the dynamic stackView. As you can see in the new animated gif, the cyan color of the "Add Item" button disappears after the first button press. To confirm, I doubled the original height of the button (i.e., from (25) to (50) at the left in the gif), which then allowed for two button pressed before it no longer worked and the cyan background disappeared. This taught me a lot about how the dynamic stackView works, and I hope it will help someone else.
I have created a UIViewController with a UIStackView (vertical axis) wrapped in a UIScrollView that's pinned to the edges of the root View with auto-layout constraints.
I have also generated a number of UIButtons and added to the arranged subviews of the UIStackView.
I have tried to no avail to centre align the UIButtons in the UIStackView.
I'm not certain what i'm doing wrong.
Here's the code:
import UIKit
class ViewController: UIViewController {
var scrollView: UIScrollView!
var stackView: UIStackView!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.whiteColor()
scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[scrollView]|", options: .AlignAllCenterX, metrics: nil, views: ["scrollView": scrollView]))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[scrollView]|", options: .AlignAllCenterX, metrics: nil, views: ["scrollView": scrollView]))
stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .Vertical
stackView.alignment = .Center
scrollView.addSubview(stackView)
scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[stackView]|", options: NSLayoutFormatOptions.AlignAllCenterX, metrics: nil, views: ["stackView": stackView]))
scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[stackView]|", options: NSLayoutFormatOptions.AlignAllCenterX, metrics: nil, views: ["stackView": stackView]))
for _ in 1 ..< 100 {
let vw = UIButton(type: UIButtonType.System)
vw.setTitle("Button", forState: .Normal)
stackView.addArrangedSubview(vw)
}
}
}
An extra equal width constraint between the scrollView and the stackView was needed. Like this:
scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:[stackView(==scrollView)]", options: .AlignAllCenterX, metrics: nil, views: ["stackView": stackView, "scrollView": scrollView]))
That did it for me.
How about:
stackView.Alignment = UIStackViewAlignment.Center
?