UIStackView: consistent height based on all subviews even if they are missing - swift

I've got a StackView with two labels as the arrangedSubviews. I want the StackView to be always the height of those two labels (including DynamicType changes), even if one of the labels text it nil or empty (which normally changes the stack views height to the height of that single label).
I tried to sketch the problem, in hope that I find a great solution. (I cannot simply put a height constraint to the stack view, because dynamicType changes the height of the labels).

I don't think you want to try to use a stack view for this.
Instead, create a custom UIView, with three labels:
Top label
Top constrained to the view Top
Bottom label
Top constrained to the bottom of the Top label (with desired space)
Bottom constrained to the view Bottom
Center label
constrained centerY to the view
Then:
If both labels have text
show them and hide the center label
If only one label has text
give them both " " as text
hide them both
show the center label
set the center label's text and font to the respective top or bottom label
Here's an example:
Custom View
class WalterView: UIView {
let labelA = UILabel()
let labelB = UILabel()
let labelC = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
for (v, c) in zip([labelA, labelB, labelC], [UIColor.cyan, UIColor.green, UIColor.yellow]) {
addSubview(v)
v.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
v.leadingAnchor.constraint(equalTo: leadingAnchor),
v.trailingAnchor.constraint(equalTo: trailingAnchor),
])
v.backgroundColor = c
v.textAlignment = .center
}
NSLayoutConstraint.activate([
labelA.topAnchor.constraint(equalTo: topAnchor),
labelB.topAnchor.constraint(equalTo: labelA.bottomAnchor, constant: 8.0),
labelB.bottomAnchor.constraint(equalTo: bottomAnchor),
labelC.centerYAnchor.constraint(equalTo: centerYAnchor),
])
labelA.text = " "
labelB.text = " "
labelC.isHidden = true
// set fonts as desired
labelA.font = .preferredFont(forTextStyle: .headline)
labelB.font = .preferredFont(forTextStyle: .subheadline)
labelA.adjustsFontForContentSizeCategory = true
labelB.adjustsFontForContentSizeCategory = true
}
func setLabels(topLabel strA: String, botLabel strB: String) -> Void {
if !strA.isEmpty && !strB.isEmpty {
// if neither string is empty
// show A & B
// set text for A & B
// hide C
labelA.text = strA
labelB.text = strB
labelC.text = ""
labelA.isHidden = false
labelB.isHidden = false
labelC.isHidden = true
} else {
// if either top or bottom string is empty
// hide A & B
// show C
// set A & B text to " " (so they're not empty)
// set text for C to the non-empty string
// set C's font & background color to respective top or bottom label
labelA.isHidden = true
labelB.isHidden = true
labelC.isHidden = false
labelA.text = " "
labelB.text = " "
if strA.isEmpty {
labelC.text = strB
labelC.backgroundColor = labelB.backgroundColor
guard let f = labelB.font else {
return
}
labelC.font = f
}
if strB.isEmpty {
labelC.text = strA
labelC.backgroundColor = labelA.backgroundColor
guard let f = labelA.font else {
return
}
labelC.font = f
}
}
}
}
Example Controller - each tap will cycle through "both", "Top Only" and "Bottom Only":
class WalterViewController: UIViewController {
var wView = WalterView()
var idx: Int = 0
var testStrings: [[String]] = [
["Top Label", "Bottom Label"],
["Top Only", ""],
["", "Bottom Only"],
]
override func viewDidLoad() {
super.viewDidLoad()
wView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(wView)
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
wView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
wView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
wView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
// NO Height constraint...
// height will be determined by wView's labels
])
// so we can see the frame of wView
wView.layer.borderWidth = 1
wView.layer.borderColor = UIColor.red.cgColor
updateLabels()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
updateLabels()
}
func updateLabels() -> Void {
let strs = testStrings[idx % testStrings.count]
wView.setLabels(topLabel: strs[0], botLabel: strs[1])
idx += 1
}
}
Results:

Related

NSLayoutConstraints not cooperating with UIImageView

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

UITableViewCell changeable content based data coming from server

I am struggling this issue which is related to UITableViewCell. I have a subclass UITableViewCell called ApplicantMessageCell and it has some subviews, labels, imageviews etc. Top part of it does not depend on the state. Just gets the data, changes labels text and imageView's image.
However for the bottom part I have completely different 3 subclasses of UIView for each state coming in. I need to show related UIView subclass at the bottom part of ApplicationMessageCell. But I could not find a way to do it.
Of course, I could create different UITableViewCell subclasses for each state but I didnot want to go that road beacuse this is just one case, I have more.
I tried to create a subclass of UIView which will behave like UILabel when it comes to resizing itself. I could not manage to it.
Lastly, I know adding each UIView subclass regarding each state and explicitly showing the one/hiding rest would solve it but I believe there are better ways to achieve this.
I did not share any code because I think this more of a theoretical question, but of course I will if anyone requests.
Thanks in advance.
Here is a quick example...
The cell class has two labels, a stack view, and 3 views (red, green, blue) with varying heights to use as the "show or not" views:
First label is constrained to the Top
Second label is constrained to the bottom of First label
stack view is constrained to the bottom of Second label and to the bottom of the cell (contentView, of course)
Three views of varying heights are then added to the stack view. Presumably, the constraints on the subviews of your different views will determine their respective heights. For this example, they are set to 40, 80 and 160.
Review the comments in the following code - it should be pretty self-explanatory:
class ApplicantMessageCell: UITableViewCell {
let titleLabel = UILabel()
let subLabel = UILabel()
let stackView = UIStackView()
let viewA = UIView()
let viewB = UIView()
let viewC = UIView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
[titleLabel, subLabel, stackView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(v)
}
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
// constrain titleLabel at top
titleLabel.topAnchor.constraint(equalTo: g.topAnchor),
titleLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
titleLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor),
// subLabel 8-pts below titleLabel
subLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8.0),
subLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
subLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor),
// stackView 8-pts below subLabel
stackView.topAnchor.constraint(equalTo: subLabel.bottomAnchor, constant: 8.0),
stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
])
// constrain stackView bottom to bottom
// this will avoid auto-layout complaints while the cells are configured
let c = stackView.bottomAnchor.constraint(equalTo: g.bottomAnchor)
c.priority = .defaultHigh
c.isActive = true
// UI element properties
stackView.axis = .vertical
stackView.spacing = 8
titleLabel.backgroundColor = .yellow
subLabel.backgroundColor = .cyan
viewA.backgroundColor = .red
viewB.backgroundColor = .green
viewC.backgroundColor = .blue
// you'll be filling the views with something to determine their heights
// but here we'll just set them to 40, 80 and 160 pts
for (v, h) in zip([viewA, viewB, viewC], [40.0, 80.0, 160.0]) {
stackView.addArrangedSubview(v)
v.heightAnchor.constraint(equalToConstant: CGFloat(h)).isActive = true
}
}
func fillData(_ top: String, sub: String, showViews: [Bool]) -> Void {
titleLabel.text = top
subLabel.text = sub
// hide views as defined in showViews array
for (v, b) in zip(stackView.arrangedSubviews, showViews) {
v.isHidden = !b
}
}
}
struct ApplicationStruct {
var title: String = ""
var subTitle: String = ""
var showViews: [Bool] = [true, true, true]
}
class FarukTableViewController: UITableViewController {
var theData: [ApplicationStruct] = []
override func viewDidLoad() {
super.viewDidLoad()
for i in 0..<20 {
// cycle through views 1, 2, 3
let b1 = i % 3 == 0
let b2 = i % 3 == 1
let b3 = i % 3 == 2
let a = [b1, b2, b3]
let d = ApplicationStruct(title: "Title \(i)", subTitle: "", showViews: a)
theData.append(d)
}
// just to test, set more than one view visible in a couple cells
theData[11].showViews = [true, false, true] // red and blue
theData[12].showViews = [false, true, true] // green and blue
theData[13].showViews = [true, true, false] // red and green
theData[14].showViews = [true, true, true] // all three
tableView.register(ApplicantMessageCell.self, forCellReuseIdentifier: "cell")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return theData.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! ApplicantMessageCell
let d = theData[indexPath.row]
let subStr = "showViews: " + d.showViews.description
c.fillData(d.title, sub: subStr, showViews: d.showViews)
return c
}
}
Result where first row shows "ViewType1" second row shows "ViewType2" and third row shows "ViewType3" ... then the rows cycle, until we hit row "Title 11" where we've set a few rows to show more than one of the "subview types":

Can we acces a UIStackView's subview's size?

My question is pretty simple but I haven't found any answer yet.
I am making a sort of table using two vertical stacks inside a horizontal stack. Both vStacks have different objects (Button in my case, with border for each one) but in the same quantity (so that they are horizontally paired like in a classic table).
I have set both of my vStack's distribution to .fillProportionally, and therefore each button have different size depending on their titleLabel length.
However, I would like to make each of my button have the same size of its paired button (the one next to him horizontally, in the other vStack) so that my cells borders would be aligned (using the biggest button's size as a reference in each pair).
I think it involves to find a way to access one stack's subview's size and then constraint the other stack subview to be equally sized. Or, because usually there is only one big button messing with the distribution and offsetting button pairs' border, accessing the way one stack displays its subviews and forcing the other stack to adopt the same way. Either way, I don't know how to do it yet.
I'd be glad if you could help me or lead me to the answer !
(I don't think I need to put code to explain my problem as it's a relatively abstract issue but if you need it I can share it)
EDIT :
Left : What I want, right : What I get
Each cell is a button (useless here but in my app it will have a functionality) with border , I want to set "description" button's height equal as "text" button. I hope it's clearer now :) I tried to invert the layout (two horizontal stacks in one vertical stack) but the issue is still here, with width instead of height this time.
EDIT 2 :
Following your advice, here is some code :
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var bottomButton: UIButton!
#IBOutlet weak var vstack: UIStackView!
#objc func buttonAction(sender: UIButton) {
sender.isEnabled = true
print(sender.frame)
}
override func viewDidLoad() {
super.viewDidLoad()
let button = newButton(text: "name")
let button2 = newButton(text: "John Smith")
let button3 = newButton(text: "Description")
let button4 = newButton(text: "text text text text text text text text text text text \n text text text text text \n text text text text text")
let hStack = UIStackView()
hStack.axis = .horizontal
hStack.distribution = .fillEqually
let hStack2 = UIStackView()
hStack2.axis = .horizontal
hStack2.distribution = .fillEqually
hStack.addArrangedSubview(button)
hStack.addArrangedSubview(button2)
hStack2.addArrangedSubview(button3)
hStack2.addArrangedSubview(button4)
vstack.addArrangedSubview(hStack)
vstack.addArrangedSubview(hStack2)
}
}
func newButton(text: String) -> UIButton {
let button = UIButton(type: .system)
button.isEnabled = true
button.setTitle(text, for: .disabled)
button.setTitle(text, for: .normal)
button.setTitleColor(.black, for: .disabled)
button.layer.borderWidth = 1
button.titleLabel?.numberOfLines = 0
button.titleLabel?.textAlignment = NSTextAlignment.center
return button
}
`
Using horizontal stacks in a vertical stack and Fill Equally partially solves the problem, because it only works when my text is under a certain length, otherwise it clips (see following image), which is why I was using fillProportionally.
OK - part of the problem is that you are modifying the titleLabel properties -- specifically, setting its .numberOfLines = 0. Auto-layout does not take that into account, without a little help.
You'll want to use a button subclass, such as this:
class MultiLineButton: UIButton {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
func commonInit() -> Void {
titleLabel?.numberOfLines = 0
titleLabel?.textAlignment = .center
// if you want to give your buttons some "padding" around the title
//contentEdgeInsets = UIEdgeInsets(top: 8.0, left: 8.0, bottom: 8.0, right: 8.0)
}
override var intrinsicContentSize: CGSize {
guard let tl = titleLabel else {
return .zero
}
let size = tl.intrinsicContentSize
return CGSize(width: size.width + contentEdgeInsets.left + contentEdgeInsets.right, height: size.height + contentEdgeInsets.top + contentEdgeInsets.bottom)
}
override func layoutSubviews() {
super.layoutSubviews()
guard let tl = titleLabel else { return }
tl.preferredMaxLayoutWidth = tl.frame.size.width
}
}
Using that class, here is an example view controller where we add a vertical stack view, and 2 horizontal "row" stack views:
class PruViewController: UIViewController {
func newButton(text: String) -> MultiLineButton {
let b = MultiLineButton()
b.titleLabel?.font = .systemFont(ofSize: 15.0)
b.setTitle(text, for: [])
b.setTitleColor(.blue, for: .normal)
b.setTitleColor(.lightGray, for: .highlighted)
b.setTitleColor(.black, for: .disabled)
b.layer.borderWidth = 1
b.layer.borderColor = UIColor.black.cgColor
return b
}
override func viewDidLoad() {
super.viewDidLoad()
let button = newButton(text: "name")
let button2 = newButton(text: "John Smith")
let button3 = newButton(text: "Description")
let button4 = newButton(text: "text text text text text text text text text text text \n text text text text text \n text text text text text")
let vStack = UIStackView()
vStack.axis = .vertical
vStack.distribution = .fill
let hStack = UIStackView()
hStack.axis = .horizontal
hStack.distribution = .fillEqually
let hStack2 = UIStackView()
hStack2.axis = .horizontal
hStack2.distribution = .fillEqually
hStack.addArrangedSubview(button)
hStack.addArrangedSubview(button2)
hStack2.addArrangedSubview(button3)
hStack2.addArrangedSubview(button4)
vStack.addArrangedSubview(hStack)
vStack.addArrangedSubview(hStack2)
vStack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(vStack)
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
vStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
vStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
vStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
}
}
The result:
And, you'll notice in my MultiLineButton class a comment about adding "padding" around the button title labels... here's how it looks with that line un-commented:

macOS NSView - How to give dynamic margin to left and right sides

I have two views A(View Controller's view), B(is a NSView and subview of A). View B pinned to top, trailing, bottom and leading of view A. When I drag window view B grows according to auto layout constrains. Which is perfectly fine. At one point I want view B to stop growing and provide margin at leading, trailing between view A and B.
I did play around widthAnchor, so the margin is grows only at right(trailing) side and which is obvious. How can I give equal margin to left(leading) side as well.
B.widthAnchor.constraint(lessThanOrEqualToConstant: 1000)
Code:
override func viewDidLoad() {
super.viewDidLoad()
view.translatesAutoresizingMaskIntoConstraints = false
let B = CustomNSView(frame: .zero)
B.translatesAutoresizingMaskIntoConstraints = false
//detailView.autoresizingMask = [.width, .maxXMargin, .maxYMargin]
// detailView.frame.size.width = 600
view.addSubview(B)
NSLayoutConstraint.activate([
B.topAnchor.constraint(equalTo: view.topAnchor),
//B.trailingAnchor.constraint(equalTo: view.trailingAnchor),
//B.widthAnchor.constraint(lessThanOrEqualToConstant: 1000),
B.bottomAnchor.constraint(equalTo: view.bottomAnchor),
B.leadingAnchor.constraint(equalTo: view.leadingAnchor),
])
}
Appreciate your inputs,
Thanks.
One way would be to save the leading and trailing layout constraints and then change their constants when the view exceeds B's max width:
var bLeading: NSLayoutConstraint?
var bTrailing: NSLayoutConstraint?
var bMaxWidth: CGFloat = 1000
override func viewDidLoad() {
super.viewDidLoad()
let b = CustomNSView()
view.addSubview(b)
b.translatesAutoresizingMaskIntoConstraints = false
b.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
b.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
bLeading = b.leadingAnchor.constraint(equalTo: view.leadingAnchor)
bTrailing = b.trailingAnchor.constraint(equalTo: view.trailingAnchor)
bLeading?.isActive = true
bTrailing?.isActive = true
}
override func viewWillLayout() {
let margin = view.bounds.width - bMaxWidth
if margin > 0 {
bLeading?.constant = margin
bTrailing?.constant = -margin
} else {
bLeading?.constant = 0
bTrailing?.constant = 0
}
}

When trait collection changes, constraint conflicts arise as though the stackview axis didn't change

I've a stackview with two controls.
When the UI is not vertically constrained:
Vertical1
When the UI is vertically constrained: Horizontal1
I get both UIs as pictured. There are no constraint conflicts when I show the UIs the first time. However, when I go from vertically constrained to vertical = regular, I get constraint conflicts.
When I comment out the stackview space (see code comment below), I don't get a constraint conflict.
class ViewController: UIViewController {
var rootStack: UIStackView!
var aggregateStack: UIStackView!
var field1: UITextField!
var field2: UITextField!
var f1f2TrailTrail: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
createIntializeViews()
createInitializeAddStacks()
}
private func createIntializeViews() {
field1 = UITextField()
field2 = UITextField()
field1.text = "test 1"
field2.text = "test 2"
}
private func createInitializeAddStacks() {
rootStack = UIStackView()
aggregateStack = UIStackView()
// If I comment out the following, there are no constraint conflicts
aggregateStack.spacing = 2
aggregateStack.addArrangedSubview(field1)
aggregateStack.addArrangedSubview(field2)
rootStack.addArrangedSubview(aggregateStack)
view.addSubview(rootStack)
rootStack.translatesAutoresizingMaskIntoConstraints = false
aggregateStack.translatesAutoresizingMaskIntoConstraints = false
field1.translatesAutoresizingMaskIntoConstraints = false
field2.translatesAutoresizingMaskIntoConstraints = false
f1f2TrailTrail = field2.trailingAnchor.constraint(equalTo: field1.trailingAnchor)
}
override public func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if traitCollection.verticalSizeClass == .regular {
aggregateStack.axis = .vertical
f1f2TrailTrail.isActive = true
} else if traitCollection.verticalSizeClass == .compact {
f1f2TrailTrail.isActive = false
aggregateStack.axis = .horizontal
} else {
print("Unexpected")
}
}
}
The constraint conflicts are here -
(
"<NSLayoutConstraint:0x600001e7d1d0 UITextField:0x7f80b2035000.trailing == UITextField:0x7f80b201d000.trailing (active)>",
"<NSLayoutConstraint:0x600001e42800 'UISV-spacing' H:[UITextField:0x7f80b201d000]-(2)-[UITextField:0x7f80b2035000] (active)>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x600001e42800 'UISV-spacing' H:[UITextField:0x7f80b201d000]-(2)-[UITextField:0x7f80b2035000] (active)>
When I place the output in www.wtfautolayout.com, I get the following:
Easier to Read Output
The second constraint shown in the above image makes me think the change to stackview vertical axis did not happen before constraints were evaluated.
Can anyone tell me what I've done wrong or how to properly set this up (without storyboard preferably)?
[EDIT] The textfields are trailing edge aligned to have this:
More of the form - portrait
More of the form - landscape
Couple notes...
There is an inherent issue with "nested" stack views causing constraint conflicts. This can be avoided by setting the priority on affected elements to 999 (instead of the default 1000).
Your layout becomes a bit complex... Labels "attached" to text fields; elements needing to be on two "lines" in portrait orientation or one "line" in landscape; one element of a "multi-element line" having a different height (the stepper); and so on.
To get your "field2" and "field3" to be equal size, you need to constrain their widths to be equal, even though they are not subviews of the same subview. This is perfectly valid, as long as they are descendants of the same view hierarchy.
Stackviews are great --- except when they're not. I would almost suggest using constraints only. You need to add more constraints, but it might avoid some issues with stack views.
However, here is an example that should get you on your way.
I've added a UIStackView subclass named LabeledFieldStackView ... it sets up the Label-above-Field in a stack view. Somewhat cleaner than mixing it in within all the other layout code.
class LabeledFieldStackView: UIStackView {
var theLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
var theField: UITextField = {
let v = UITextField()
v.translatesAutoresizingMaskIntoConstraints = false
v.borderStyle = .roundedRect
return v
}()
convenience init(with labelText: String, fieldText: String, verticalGap: CGFloat) {
self.init()
axis = .vertical
alignment = .fill
distribution = .fill
spacing = 2
addArrangedSubview(theLabel)
addArrangedSubview(theField)
theLabel.text = labelText
theField.text = fieldText
self.translatesAutoresizingMaskIntoConstraints = false
}
}
class LargentViewController: UIViewController {
var rootStack: UIStackView!
var fieldStackView1: LabeledFieldStackView!
var fieldStackView2: LabeledFieldStackView!
var fieldStackView3: LabeledFieldStackView!
var fieldStackView4: LabeledFieldStackView!
var stepper: UIStepper!
var fieldAndStepperStack: UIStackView!
var twoLineStack: UIStackView!
var fieldAndStepperStackWidthConstraint: NSLayoutConstraint!
// horizontal gap between elements on the same "line"
var horizontalSpacing: CGFloat!
// vertical gap between "lines"
var verticalSpacing: CGFloat!
// vertical gap between labels above text fields
var labelToFieldSpacing: CGFloat!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
horizontalSpacing = CGFloat(2)
verticalSpacing = CGFloat(8)
labelToFieldSpacing = CGFloat(2)
createIntializeViews()
createInitializeStacks()
fillStacks()
}
private func createIntializeViews() {
fieldStackView1 = LabeledFieldStackView(with: "label 1", fieldText: "field 1", verticalGap: labelToFieldSpacing)
fieldStackView2 = LabeledFieldStackView(with: "label 2", fieldText: "field 2", verticalGap: labelToFieldSpacing)
fieldStackView3 = LabeledFieldStackView(with: "label 3", fieldText: "field 3", verticalGap: labelToFieldSpacing)
fieldStackView4 = LabeledFieldStackView(with: "label 4", fieldText: "field 4", verticalGap: labelToFieldSpacing)
stepper = UIStepper()
}
private func createInitializeStacks() {
rootStack = UIStackView()
fieldAndStepperStack = UIStackView()
twoLineStack = UIStackView()
[rootStack, fieldAndStepperStack, twoLineStack].forEach {
$0?.translatesAutoresizingMaskIntoConstraints = false
}
// rootStack has spacing of horizontalSpacing (inter-line vertical spacing)
rootStack.axis = .vertical
rootStack.alignment = .fill
rootStack.distribution = .fill
rootStack.spacing = verticalSpacing
// fieldAndStepperStack has spacing of horizontalSpacing (space between field and stepper)
// and .alignment of .bottom (so stepper aligns vertically with field)
fieldAndStepperStack.axis = .horizontal
fieldAndStepperStack.alignment = .bottom
fieldAndStepperStack.distribution = .fill
fieldAndStepperStack.spacing = horizontalSpacing
// twoLineStack has inter-line vertical spacing of
// verticalSpacing in portrait orientation
// for landscape orientation, the two "lines" will be changed to one "line"
// and the spacing will be changed to horizontalSpacing
twoLineStack.axis = .vertical
twoLineStack.alignment = .leading
twoLineStack.distribution = .fill
twoLineStack.spacing = verticalSpacing
}
private func fillStacks() {
self.view.addSubview(rootStack)
// constrain rootStack Top, Leading, Trailing = 20
// no height or bottom constraint
NSLayoutConstraint.activate([
rootStack.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20.0),
rootStack.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20.0),
rootStack.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20.0),
])
rootStack.addArrangedSubview(fieldStackView1)
fieldAndStepperStack.addArrangedSubview(fieldStackView2)
fieldAndStepperStack.addArrangedSubview(stepper)
twoLineStack.addArrangedSubview(fieldAndStepperStack)
twoLineStack.addArrangedSubview(fieldStackView3)
rootStack.addArrangedSubview(twoLineStack)
// fieldAndStepperStack needs width constrained to its superview (the twoLineStack) when
// in portrait orientation
// setting the priority to 999 prevents "nested stackView" constraint breaks
fieldAndStepperStackWidthConstraint = fieldAndStepperStack.widthAnchor.constraint(equalTo: twoLineStack.widthAnchor, multiplier: 1.0)
fieldAndStepperStackWidthConstraint.priority = UILayoutPriority(rawValue: 999)
// constrain fieldView3 width to fieldView2 width to keep them the same size
NSLayoutConstraint.activate([
fieldStackView3.widthAnchor.constraint(equalTo: fieldStackView2.widthAnchor, multiplier: 1.0)
])
rootStack.addArrangedSubview(fieldStackView4)
}
override public func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if traitCollection.verticalSizeClass == .regular {
fieldAndStepperStackWidthConstraint.isActive = true
twoLineStack.axis = .vertical
twoLineStack.spacing = verticalSpacing
} else if traitCollection.verticalSizeClass == .compact {
fieldAndStepperStackWidthConstraint.isActive = false
twoLineStack.axis = .horizontal
twoLineStack.spacing = horizontalSpacing
} else {
print("Unexpected")
}
}
}
And the results:
When a UIView is added to a UIStackView, the stackView will assign constraints to that view based on the properties assigned to the stackView (axis, alignment, distribution, spacing). As mentioned by #DonMag you are adding a constraint to the textField's in the aggregateStack view. The aggregateStack will add its own constraints based on it attributes. By removing that constraint and the activation/deactivation code the constraint conflict goes away.
I created a small example using your code and adding some background views to the stackViews so you can see more easily what is happening when you change the various properties. Just for illustration I pinned the rootStackView to the edges of the view controller's view, just so it would be visible.
import UIKit
class StackViewController: UIViewController {
var rootStack: UIStackView!
var aggregateStack: UIStackView!
var field1: UITextField!
var field2: UITextField!
var f1f2TrailTrail: NSLayoutConstraint!
private lazy var backgroundView: UIView = {
let view = UIView()
view.backgroundColor = .purple
view.layer.cornerRadius = 10.0
return view
}()
private lazy var otherBackgroundView: UIView = {
let view = UIView()
view.backgroundColor = .green
view.layer.cornerRadius = 10.0
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
createIntializeViews()
createInitializeAddStacks()
}
private func createIntializeViews() {
field1 = UITextField()
field1.backgroundColor = .orange
field2 = UITextField()
field2.backgroundColor = .blue
field1.text = "test 1"
field2.text = "test 2"
}
private func createInitializeAddStacks() {
rootStack = UIStackView()
rootStack.alignment = .center
rootStack.distribution = .fillProportionally
pinBackground(backgroundView, to: rootStack)
aggregateStack = UIStackView()
aggregateStack.alignment = .center
aggregateStack.distribution = .fillProportionally
pinBackground(otherBackgroundView, to: aggregateStack)
// If I comment out the following, there are no constraint conflicts
aggregateStack.spacing = 5
field1.translatesAutoresizingMaskIntoConstraints = false
field2.translatesAutoresizingMaskIntoConstraints = false
aggregateStack.addArrangedSubview(field1)
aggregateStack.addArrangedSubview(field2)
rootStack.addArrangedSubview(aggregateStack)
view.addSubview(rootStack)
rootStack.translatesAutoresizingMaskIntoConstraints = false
/**
* pin the root stackview to the edges of the view controller, just so we can see
* it's behavior
*/
NSLayoutConstraint.activate([
rootStack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant:16),
rootStack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant:-16),
rootStack.topAnchor.constraint(equalTo: view.topAnchor, constant:32),
rootStack.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant:-32),
])
}
/**
* Inserts a UIView into the UIStackView's hierarchy, but not as part of the arranged subviews
* see https://useyourloaf.com/blog/stack-view-background-color/
*/
private func pinBackground(_ view: UIView, to stackView: UIStackView) {
view.translatesAutoresizingMaskIntoConstraints = false
stackView.insertSubview(view, at: 0)
NSLayoutConstraint.activate([
view.leadingAnchor.constraint(equalTo: stackView.leadingAnchor),
view.trailingAnchor.constraint(equalTo: stackView.trailingAnchor),
view.topAnchor.constraint(equalTo: stackView.topAnchor),
view.bottomAnchor.constraint(equalTo: stackView.bottomAnchor)
])
}
override public func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
switch traitCollection.verticalSizeClass {
case .regular:
aggregateStack.axis = .vertical
case .compact:
aggregateStack.axis = .horizontal
case .unspecified:
print("Unexpected")
}
}
}