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
}
}
Related
I'm creating a vertical UIStackView where arranged subviews will start from top. Subviews quantity will be 5 at most. Here are my expectation, reality and code. Any Idea?
Expectation
Current situation
Code
var homeVStack: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.alignment = .top
stackView.distribution = .equalSpacing
stackView.spacing = 20
stackView.translatesAutoresizingMaskIntoConstraints = false
return stackView
}()
private func loadData() {
if let homeFormList = data?.homeTeamForm {
for homeForm in homeFormList {
let teamFormView = SimpleScoreView()
teamFormView.teamForm = homeForm
teamFormView.heightAnchor.constraint(equalToConstant: 50).isActive = true
teamFormView.backgroundColor = .yellow
homeVStack.addArrangedSubview(teamFormView)
}
}
}
Use empty view like UIView() on your last arranged view. It should be made larger than its intrinsic size.
var homeVStack: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.alignment = .fill // default is .fill
stackView.distribution = .fill
stackView.spacing = 20
stackView.translatesAutoresizingMaskIntoConstraints = false
return stackView
}()
private func loadData() {
if let homeFormList = data?.homeTeamForm {
for homeForm in homeFormList {
let teamFormView = SimpleScoreView()
teamFormView.teamForm = homeForm
teamFormView.heightAnchor.constraint(equalToConstant: 50).isActive = true
teamFormView.backgroundColor = .yellow
homeVStack.addArrangedSubview(teamFormView)
}
}
homeVStack.addArrangedSubview(UIView()) // Important
}
Above code make your stack view layout like this. UIView should be stretched than SimpleScoreView's intrinsic size
Current Situation image said homeVStacks constraints are top, bot, leading and trailing to its superview with equal spacing so second SimpleScoreView is on bottom.
You can choice two options
You should make spacing with another UI like transparent UIView for stretched instead of SimpleScoreView bottom layout.
Remove stack view's bottom constraint to root view and make height by contents(arranged view)
You are explicitly setting the Height of the subviews:
teamFormView.heightAnchor.constraint(equalToConstant: 50).isActive = true
So, do NOT give your stackView a Bottom anchor or Height constraint.
Here's a quick example...
SimpleScoreView -- view with a centered label:
class SimpleScoreView: UIView {
let scoreLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
self.backgroundColor = .yellow
scoreLabel.textAlignment = .center
scoreLabel.translatesAutoresizingMaskIntoConstraints = false
scoreLabel.backgroundColor = .green
addSubview(scoreLabel)
NSLayoutConstraint.activate([
scoreLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
scoreLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
}
Basic View Controller -- each tap will add another "Score View":
class ViewController: UIViewController {
var homeVStack: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
// these two are not needed
//stackView.alignment = .top
//stackView.distribution = .equalSpacing
stackView.spacing = 20
stackView.translatesAutoresizingMaskIntoConstraints = false
return stackView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(homeVStack)
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain stack view Top / Leading / Trailing
homeVStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 8.0),
homeVStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
homeVStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
])
loadData()
}
// for this example, loadData() will add one new
// SimpleScoreView each time it's called
private func loadData() {
let v = SimpleScoreView()
v.heightAnchor.constraint(equalToConstant: 50).isActive = true
v.scoreLabel.text = "\(homeVStack.arrangedSubviews.count + 1)"
homeVStack.addArrangedSubview(v)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
loadData()
}
}
Results:
what are the constraints of homeVStack? make it center horizontally and vertically in super View.
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:
I have buttons inside a view which in potrait mode I want like this -
which is achieved by the following code -
//original potrait mode/////
import UIKit
class PotraitViewController: UIViewController {
override func viewDidLoad() {
let buttonred = UIButton()
buttonred.backgroundColor = UIColor.red
let buttonblue = UIButton()
buttonblue.backgroundColor = UIColor.blue
let landscapesmallview = UIView()
view.addSubview(landscapesmallview)
landscapesmallview.addSubview(buttonred)
landscapesmallview.addSubview(buttonblue)
buttonred.translatesAutoresizingMaskIntoConstraints = false
buttonblue.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
buttonred.topAnchor.constraint(equalTo: view.topAnchor,constant: 200),
buttonred.centerXAnchor.constraint(equalTo: view.centerXAnchor),
buttonred.trailingAnchor.constraint(equalTo: view.trailingAnchor,constant:-20),
buttonred.widthAnchor.constraint(equalToConstant: 50),
//-------
buttonblue.topAnchor.constraint(equalTo: buttonred.bottomAnchor,constant: 40),
buttonblue.leadingAnchor.constraint(equalTo: buttonred.leadingAnchor),
buttonblue.trailingAnchor.constraint(equalTo:buttonred.trailingAnchor),
buttonblue.widthAnchor.constraint(equalTo: buttonred.widthAnchor)
])
}
}
and in landscape mode I want like this -
which is achieved by the following code -
// original lanscape mode/////
import UIKit
class LandscapeViewController: UIViewController {
override func viewDidLoad() {
let buttonred = UIButton()
buttonred.backgroundColor = UIColor.red
let buttonblue = UIButton()
buttonblue.backgroundColor = UIColor.blue
view.addSubview(buttonred)
view.addSubview(buttonblue)
buttonred.translatesAutoresizingMaskIntoConstraints = false
buttonblue.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
buttonred.centerYAnchor.constraint(equalTo: view.centerYAnchor),
buttonred.leadingAnchor.constraint(equalTo: view.leadingAnchor,constant:40),
buttonred.trailingAnchor.constraint(equalTo: view.centerXAnchor,constant:-20),
buttonred.widthAnchor.constraint(equalToConstant: 50),
//-------
buttonblue.centerYAnchor.constraint(equalTo: buttonred.centerYAnchor),
buttonblue.leadingAnchor.constraint(equalTo: view.centerXAnchor,constant:40),
buttonblue.trailingAnchor.constraint(equalTo: view.trailingAnchor,constant:-20),
buttonblue.widthAnchor.constraint(equalTo:buttonred.widthAnchor)
])
}
}
So, I tried the following code to achieve by screen rotation i.e. two different layouts in potrait and landscape views programmatically with the help of the following code:-
import UIKit
class NewViewController: UIViewController {
override func viewDidLoad() {
let buttonredlandscape = UIButton()
buttonredlandscape.backgroundColor = UIColor.red
let buttonbluelandscape = UIButton()
buttonbluelandscape.backgroundColor = UIColor.blue
let buttonredportrait = UIButton()
buttonredportrait.backgroundColor = UIColor.red
let buttonblueportrait = UIButton()
buttonblueportrait.backgroundColor = UIColor.blue
let landscapesmallview = UIView()
let portraitsmallview = UIView()
landscapesmallview.backgroundColor = UIColor.gray
portraitsmallview.backgroundColor = UIColor.purple
landscapesmallview.frame = view.frame
portraitsmallview.frame = view.frame
view.addSubview(landscapesmallview)
view.addSubview(portraitsmallview)
landscapesmallview.addSubview(buttonredlandscape)
landscapesmallview.addSubview(buttonbluelandscape)
portraitsmallview.addSubview(buttonredportrait)
portraitsmallview.addSubview(buttonblueportrait)
buttonredlandscape.translatesAutoresizingMaskIntoConstraints = false
buttonbluelandscape.translatesAutoresizingMaskIntoConstraints = false
buttonredportrait.translatesAutoresizingMaskIntoConstraints = false
buttonblueportrait.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
buttonredlandscape.centerYAnchor.constraint(equalTo:landscapesmallview.centerYAnchor),
buttonredlandscape.topAnchor.constraint(equalTo:landscapesmallview.topAnchor,constant:40),
buttonredlandscape.trailingAnchor.constraint(equalTo: landscapesmallview.centerXAnchor,constant:-20),
buttonredlandscape.heightAnchor.constraint(equalTo: landscapesmallview.heightAnchor,constant:50),
buttonbluelandscape.centerYAnchor.constraint(equalTo:buttonredlandscape.centerYAnchor),
buttonbluelandscape.leadingAnchor.constraint(equalTo: landscapesmallview.centerXAnchor,constant:40),
buttonbluelandscape.trailingAnchor.constraint(equalTo: landscapesmallview.trailingAnchor,constant:-20),
buttonbluelandscape.heightAnchor.constraint(equalTo: buttonredlandscape.heightAnchor),
buttonredportrait.topAnchor.constraint(equalTo: portraitsmallview.topAnchor,constant: 200),
buttonredportrait.centerXAnchor.constraint(equalTo: portraitsmallview.centerXAnchor),
buttonredportrait.trailingAnchor.constraint(equalTo: portraitsmallview.trailingAnchor,constant:-20),
buttonredportrait.widthAnchor.constraint(equalTo: buttonredportrait.widthAnchor),
buttonblueportrait.topAnchor.constraint(equalTo: buttonredportrait.bottomAnchor,constant: 40),
buttonblueportrait.leadingAnchor.constraint(equalTo: buttonredportrait.leadingAnchor),
buttonblueportrait.trailingAnchor.constraint(equalTo:buttonredportrait.trailingAnchor),
buttonblueportrait.widthAnchor.constraint(equalTo: buttonredportrait.widthAnchor)
])
//-------
func viewWillTransition(to size: CGSize, with coordinator: ) {
if UIDevice.current.orientation.isLandscape {
landscapesmallview.translatesAutoresizingMaskIntoConstraints = false
portraitsmallview.translatesAutoresizingMaskIntoConstraints = true
} else if UIDevice.current.orientation.isPortrait {
portraitsmallview.translatesAutoresizingMaskIntoConstraints = false
landscapesmallview.translatesAutoresizingMaskIntoConstraints = true
}
}
}
}
which in potrait mode shows -
and which in landscape mode shows -
How to achieve what I want programmatically i.e. topmost 2 buttons to rearrange themselves programmatically every-time the user rotates the device. Its not just the buttons. It can be labels, images, collectionview etc. or just anything. The upshot is that I want to achieve two different layouts in landscape and portrait modes programmatically irrespective of the device.
Points to be noted :-
i) I have tried used NSLayoutAnchor with "NSLayoutConstraint.activate" because apple recommends it, but if the code can be made shorter(and faster) with some other method like visual format etc. I'm okay with that as well/
ii) If possible, I do not want to use stackview or containerview, because there can be many more types of labels, buttons etc, but if there is no other way, then I will use it.
iii) Is my code DRY principle compliant ?
Also, guys, please, I do not deserve negative marks because, as far as I know, this has not been asked before. I request you not to give negative marks and encourage me.
There are various ways to do this. One approach:
declare two "constraint" arrays
one to hold the "narrow view" constraints
one to hold the "wide view" constraints
activate / deactivate the constraints as needed
Here is a complete example:
class ChangeLayoutViewController: UIViewController {
let redButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .red
v.setTitle("Red Button", for: [])
return v
}()
let blueButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .blue
v.setTitle("Blue Button", for: [])
return v
}()
var narrowConstraints: [NSLayoutConstraint] = [NSLayoutConstraint]()
var wideConstraints: [NSLayoutConstraint] = [NSLayoutConstraint]()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(redButton)
view.addSubview(blueButton)
let g = view.safeAreaLayoutGuide
var c: NSLayoutConstraint
// MARK: - narrow orientation
// constrain redButton above blueButton
// constrain redButton leading and trailing to safe-area (with 8-pts on each side)
c = redButton.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0)
narrowConstraints.append(c)
c = redButton.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0)
narrowConstraints.append(c)
// constrain blueButton leading and trailing to safe-area (with 8-pts on each side)
c = blueButton.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0)
narrowConstraints.append(c)
c = blueButton.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0)
narrowConstraints.append(c)
// constrain redButton top 40-pts from safe-area top
c = redButton.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0)
narrowConstraints.append(c)
// constrain blueButton top 20-pts from redButton bottom
c = blueButton.topAnchor.constraint(equalTo: redButton.bottomAnchor, constant: 20.0)
narrowConstraints.append(c)
// MARK: - wide orientation
// constrain redButton & blueButton side-by-side
// with equal widths and 8-pts between them
// constrain redButton leading 8-pts from safe-area leading
c = redButton.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0)
wideConstraints.append(c)
// constrain blueButton trailing 8-pts from safe-area trailing
c = blueButton.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0)
wideConstraints.append(c)
// constrain blueButton leading 8-pts from redButton trailing
c = blueButton.leadingAnchor.constraint(equalTo: redButton.trailingAnchor, constant: 8.0)
wideConstraints.append(c)
// constrain buttons to equal widths
c = blueButton.widthAnchor.constraint(equalTo: redButton.widthAnchor)
wideConstraints.append(c)
// constrain both buttons centerY to safe-area centerY
c = redButton.centerYAnchor.constraint(equalTo: g.centerYAnchor)
wideConstraints.append(c)
c = blueButton.centerYAnchor.constraint(equalTo: g.centerYAnchor)
wideConstraints.append(c)
// activate initial constraints based on view width:height ratio
changeConstraints(view.frame.width > view.frame.height)
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
// change active set of constraints based on view width:height ratio
self.changeConstraints(size.width > size.height)
}
func changeConstraints(_ useWide: Bool) -> Void {
if useWide {
NSLayoutConstraint.deactivate(narrowConstraints)
NSLayoutConstraint.activate(wideConstraints)
} else {
NSLayoutConstraint.deactivate(wideConstraints)
NSLayoutConstraint.activate(narrowConstraints)
}
}
}
Results:
refer this this image for handling in device orientation
take both buttons in stack view and make the stack view in center vertically and center horizontally
I have a horizontal stack view with 3 buttons: Backwards, Play, Forward for a music application.
Here is my current code:
self.controlStackView.axis = .horizontal
self.controlStackView.distribution = .equalSpacing
self.controlStackView.alignment = .center
self.controlStackView.spacing = 10.0
self.controlStackView.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(self.controlStackView)
self.controlStackView.topAnchor.constraint(equalTo: self.artworkImageView.bottomAnchor, constant: 10.0).isActive = true
self.controlStackView.centerXAnchor.constraint(equalTo: self.contentView.centerXAnchor).isActive = true
What does this is it distributes the button as follows (from the center due to alignment):
[backward] - 10 spacing - [play] - 10 spacing - [forward]
I could increase the spacing but it would still be fixed.
So I'm setting the leading and trailing anchor to define a maximum width of the stack view:
self.controlStackView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
self.controlStackView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
What this does to the layout:
[left edge backward] - lots of spaaaaaaace - [centered play] - lots of spaaaaaaace - [right edge forward]
It distributes over the entire width (and there is an .equalSpacing between center to the left and right). But this also is not helpful. Essentially my goal is to have true equal spacing including the edges.
Let's say I have an available width of 100 and my 3 buttons are 10, 20, 10 - which means that there is 60 remaining space that is empty.
I would like it to be distributed like this:
space - [backward] - space - [play] - space [forward] - space
So 4 spaces in between my buttons each space being 15, so we fill the 60 remaining space.
I could of course implement padding to the stack view to get the outer space, but this would be quite static and is not equally distributed.
Does anybody know if I can implement it this way that the edges are included into the space distribution?
Thanks
This is really pretty straight-forward, using "spacer" views.
Add one more spacer than the number of buttons, so you have:
spacer - button - spacer - button - spacer
Then, constrain the widths of spacers 2-to-n equal to the width of the first spacer. The stackView will handle the rest!
Here is an example (just needs a viewController in storyboard, the rest is done via code):
class DistributeViewController: UIViewController {
let stackView: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .horizontal
v.alignment = .fill
v.distribution = .fill
v.spacing = 0
return v
}()
var buttonTitles = [
"Backward",
"Play",
"Forward",
// "Next",
]
var numButtons = 0
override func viewDidLoad() {
super.viewDidLoad()
// stackView will hold the buttons and spacers
view.addSubview(stackView)
// constrain it to Top + 20, Leading and Trailing == 0, height will be controlled by button height
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20.0),
stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 0.0),
stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 0.0),
])
// arrays to hold our buttons and spacers
var buttons: [UIView] = [UIView]()
var spacers: [UIView] = [UIView]()
numButtons = buttonTitles.count
// create the buttons and append them to our buttons array
for i in 0..<numButtons {
let b = UIButton()
b.translatesAutoresizingMaskIntoConstraints = false
b.backgroundColor = .blue
b.setTitle(buttonTitles[i], for: .normal)
buttons.append(b)
}
// create the spacer views and append them to our spacers array
// we need 1 more spacer than buttons
for _ in 1...numButtons+1 {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .red // just so we can see them... use .clear for production
spacers.append(v)
}
// addd spacers and buttons to stackView
for i in 0..<spacers.count {
stackView.addArrangedSubview(spacers[i])
// one fewer buttons than spacers, so don't go out-of-range
if i < buttons.count {
stackView.addArrangedSubview(buttons[i])
}
if i > 0 {
// constrain spacer widths to first spacer's width (this will make them all equal)
spacers[i].widthAnchor.constraint(equalTo: spacers[0].widthAnchor, multiplier: 1.0).isActive = true
// if you want the buttons to be equal widths, uncomment this block
/*
if i < buttons.count {
buttons[i].widthAnchor.constraint(equalTo: buttons[0].widthAnchor, multiplier: 1.0).isActive = true
}
*/
}
}
}
}
The results with 3 buttons:
and with 4 buttons:
and a couple with equal-width buttons:
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")
}
}
}