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:
Couple notes...
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: