UIStackView equal spacing including edges - swift

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:

Related

More complicated AutoLayout equations

Hi Please take a look at the following mockup:
I wanted to know how I can create the constraint from above:
V2.top = C1.top + n * V1.height
Because this is not something like the default equation for constraints:
item1.attribute1 = multiplier × item2.attribute2 + constant
I know I can just use AutoResizingMask but it will create a real mess in my code because my code is very complicated, and I also don't like AutoResizingMask that much.
(by the way, please answer in Swift only!)
Thank you
You can do this with a UILayoutGuide -- from Apple's docs:
The UILayoutGuide class is designed to perform all the tasks previously performed by dummy views, but to do it in a safer, more efficient manner.
To get your desired layout, we can:
add a layout guide to C1
constrain its Top to C1 Top
constrain its Height to V1 Height with a "n" multiplier
constrain V2 Top to the guide's Bottom
Here is a complete example to demonstrate:
class GuideViewController: UIViewController {
// a label on each side so we can
// "tap to change" v1 Height and "n" multiplier
let labelN = UILabel()
let labelH = UILabel()
let containerView = UIView()
let v1 = UILabel()
let v2 = UILabel()
// a layout guide for v2's Top spacing
let layG = UILayoutGuide()
// we'll change these on taps
var n:CGFloat = 0
var v1H: CGFloat = 30
// constraints we'll want to modify when "n" or "v1H" change
var v1HeightConstraint: NSLayoutConstraint!
var layGHeightConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
v1.text = "V1"
v2.text = "V2"
v1.textAlignment = .center
v2.textAlignment = .center
containerView.backgroundColor = .systemTeal
v1.backgroundColor = .green
v2.backgroundColor = .yellow
[containerView, v1, v2].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
containerView.addSubview(v1)
containerView.addSubview(v2)
view.addSubview(containerView)
// add the layout guide to containerView
containerView.addLayoutGuide(layG)
// respect safe area
let safeG = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// let's give the container 80-pts Top/Bottom and 120-pts on each side
containerView.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 80.0),
containerView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 120.0),
containerView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: -120.0),
containerView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: -80.0),
// v1 Leading / Trailing / Bottom 20-pts
v1.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20.0),
v1.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20.0),
v1.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -20.0),
// just use v2's intrinisic height
// v2 Leading / Trailing 20-pts
v2.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20.0),
v2.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20.0),
// layout Guide Top / Leading / Trailing
layG.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 0.0),
layG.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 0.0),
layG.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 0.0),
// and constrain v2 Top to layout Guide Bottom
v2.topAnchor.constraint(equalTo: layG.bottomAnchor, constant: 0.0),
])
// layout Guide Height equals v1 Height x n
layGHeightConstraint = layG.heightAnchor.constraint(equalTo: v1.heightAnchor, multiplier: n)
layGHeightConstraint.isActive = true
// v1 Height
v1HeightConstraint = v1.heightAnchor.constraint(equalToConstant: v1H)
v1HeightConstraint.isActive = true
// "tap to change" labels
[labelN, labelH].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
$0.textAlignment = .center
$0.numberOfLines = 0
view.addSubview($0)
let t = UITapGestureRecognizer(target: self, action: #selector(tapHandler(_:)))
$0.addGestureRecognizer(t)
$0.isUserInteractionEnabled = true
}
NSLayoutConstraint.activate([
labelN.topAnchor.constraint(equalTo: containerView.topAnchor),
labelN.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 8.0),
labelN.trailingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: -8.0),
labelN.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
labelH.topAnchor.constraint(equalTo: containerView.topAnchor),
labelH.leadingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 8.0),
labelH.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: -8.0),
labelH.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
])
updateInfo()
}
#objc func tapHandler(_ gr: UITapGestureRecognizer) -> Void {
guard let v = gr.view else {
return
}
// if we tapped on the "cylcle N" label
if v == labelN {
n += 1
if n == 6 {
n = 0
}
// can't change multiplier directly, so
// de-Activate / set it / Activate
layGHeightConstraint.isActive = false
layGHeightConstraint = layG.heightAnchor.constraint(equalTo: v1.heightAnchor, multiplier: n)
layGHeightConstraint.isActive = true
}
// if we tapped on the "cylcle v1H" label
if v == labelH {
v1H += 5
if v1H > 50 {
v1H = 30
}
v1HeightConstraint.constant = v1H
}
updateInfo()
}
func updateInfo() -> Void {
var s: String = ""
s = "Tap to cycle \"n\" from Zero to 5\n\nn = \(n)"
labelN.text = s
s = "Tap to cycle \"v1H\" from 30 to 50\n\nv1H = \(v1H)"
labelH.text = s
}
}
When you run it, it will look like this:
Each time you tap the left side, it will cycle the n multiplier variable from Zero to 5, and update the constraints.
Each time you tap the right side, it will cycle the v1H height variable from 30 to 50, and update the constraints.
It can be solved by using a helper view.
A helper view is in this case just a UIView used for sizing purpose, without visible content of its own. Either set its alpha = 0 or hidden = true.
Set helperView.top = c1.top
Set helperView.height = v1.height
Set v2.top = helperView.bottom + 5
You also need to set the width and leading for the helper view, but their values are not important.

Swift - Set the height of the item as same as parent in nested UIStackView programmingly

I have a UI with 3 main parts: header, keypad and a button.
Size of header and button are fixed, the remaining area should be occupied by keypad.
Like this:
The keypad part is build up by a nested UIStackView (vertical UIStackView parent, with 4 horizontal UIStackView children), 3 buttons will be added to each of the horizontal UIStackView.
Everything is fine except I would like the buttons to have the same height as the horizontal UIStackView, so that it should be easier for user to click on it.
I have tried
rowStackView.alignment = .fill
or
for button in numberButtons
{
button.translatesAutoresizingMaskIntoConstraints = false
button.heightAnchor.constraint(equalTo: button.superview!.heightAnchor).isActive = true
}
However, the numberPadStackView will be squeezed like following. How should I fix that? Thanks.
These are my codes for now:
headerStackView.axis = .vertical
headerStackView.alignment = .center
headerStackView.setContentHuggingPriority(.defaultHigh, for: .vertical)
headerStackView.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
view.addSubview(headerStackView)
numberPadStackView.axis = .vertical
numberPadStackView.distribution = .fillEqually
numberPadStackView.alignment = .center
view.addSubview(numberPadStackView)
initNumpad()
view.addSubview(requestLabel)
initNumPad()
private func initNumpad()
{
var rowStackView = UIStackView()
numberButtons.removeAll()
for i in 0 ..< 11
{
if i % 3 == 0
{
rowStackView = UIStackView()
rowStackView.axis = .horizontal
rowStackView.distribution = .fillEqually
rowStackView.alignment = .center
rowStackView.setContentHuggingPriority(.defaultLow, for: .vertical)
numberPadRowStackView.append(rowStackView)
numberPadStackView.addArrangedSubview(rowStackView)
}
let button = UIButton()
switch i
{
case 0 ..< 9:
button.setTitle("\(i + 1)", for: .normal)
case 9:
button.setTitle(".", for: .normal)
case 10:
button.setTitle("0", for: .normal)
default:
return
}
button.titleLabel?.textAlignment = .center
button.setContentHuggingPriority(.defaultLow, for: .vertical)
button.backgroundColor = UIColor.random()
numberButtons.append(button)
rowStackView.addArrangedSubview(button)
}
numberPadDeleteImageView.backgroundColor = UIColor.random()
rowStackView.addArrangedSubview(numberPadDeleteImageView)
}
Layout
headerStackView.translatesAutoresizingMaskIntoConstraints = false
numberPadStackView.translatesAutoresizingMaskIntoConstraints = false
requestLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
headerStackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 24),
headerStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Padding),
headerStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Padding),
numberPadStackView.topAnchor.constraint(equalTo: headerStackView.bottomAnchor, constant: 43),
numberPadStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Padding),
numberPadStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Padding),
requestLabel.topAnchor.constraint(equalTo: numberPadStackView.bottomAnchor, constant: 21),
requestLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 28),
requestLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -28),
requestLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -5),
requestLabel.heightAnchor.constraint(equalToConstant: 40),
])
for rowStackView in numberPadRowStackView
{
rowStackView.translatesAutoresizingMaskIntoConstraints = false
rowStackView.widthAnchor.constraint(equalTo: numberPadStackView.widthAnchor).isActive = true
}
Couple notes...
Setting Content Hugging Priority on a UIStackView is generally not going to give you the expected results. That's because the stack view is arranging its subviews (based on the stack view's Alignment and Distribution properties). The Content Hugging Priority of the stack view's arranged subviews will be the controlling factor.
As is obvious, if I lay out 4 labels, vertically constrained top-to-bottom, each having the same Content Hugging priority (such as the default 250), I'll get something like this in Storyboard (note the Red layout-problem indicator):
and at runtime it may look like this:
Auto-layout is going to respect the (intrinsic) Height for Labels 1, 2 and 4, and then stretch Label 3 to complete the layout.
If I embed the top two and bottom two labels each in vertical stack views...
Storyboard will look like this (again, note the Red layout-problem indicator):
and we get the same thing at run-time:
Even if I set the Content Hugging Priority of the top stack view to 1000, it won't make a difference -- because auto-layout is using the arranged subviews to decide what to do.
So, for your layout, divide your screen into *three layout elements:
the "header" section
the "numberPad" section
the "process" section
and then tell auto-layout you want the Header and Process elements to maintain their heights, and allow the numberPad to stretch.
Since UI elements default to Hugging Priority of 250, probably the easiest way to manage that is reduce the Hugging Priority of the numberPad buttons.
Here is some example code. I'm not sure how you're laying out your "headerStackView" as it doesn't really look like it would lend itself to a stack view... so, I laid it out as a header UIView:
class WithNumpadViewController: UIViewController {
// three "parts" to our layout
let headerView = UIView()
let numberPadStackView = UIStackView()
let requestLabel = UILabel()
// data labels to be filled-in
let currencyLabel = UILabel()
let currValueLabel = UILabel()
let balanceLabel = UILabel()
// however you're using this
var numberButtons: [UIButton] = []
let Padding: CGFloat = 16
override func viewDidLoad() {
super.viewDidLoad()
if let vc = self.navigationController?.viewControllers.first {
vc.navigationItem.title = "Wallet"
}
self.navigationController?.navigationBar.barTintColor = .black
self.navigationController?.navigationBar.tintColor = .white
self.navigationController?.navigationBar.isTranslucent = false
self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
title = "Withdraw"
// add "three parts" to view
[headerView, numberPadStackView, requestLabel].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain headerView to Top / Leading / Trailing (safe-area)
// let its content determine its height
headerView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
headerView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
headerView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
// constrain numberPad 40-pts from Bottom headerView
numberPadStackView.topAnchor.constraint(equalTo: headerView.bottomAnchor, constant: 40),
// Leading / Trailing with Padding
numberPadStackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: Padding),
numberPadStackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -Padding),
// constrain requestLabel 21-pts from Bottom of numberPad
requestLabel.topAnchor.constraint(equalTo: numberPadStackView.bottomAnchor, constant: 21),
// Leading / Trailing with 28-pts padding
requestLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 28),
requestLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -28),
// Bottom 5-pts from Bottom (safe-area)
requestLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -5),
// explicit Height of 40-pts
requestLabel.heightAnchor.constraint(equalToConstant: 40),
])
// setup contents of headerView
initHeader()
// setup contents of numberPad
initNumpad()
// properties for requestLabel
requestLabel.backgroundColor = .black
requestLabel.textColor = .white
requestLabel.textAlignment = .center
requestLabel.text = "Process"
// fill-in data labels
currencyLabel.text = "HKD"
currValueLabel.text = "0"
balanceLabel.text = "Balance: HKD 2 (Available)"
// maybe add number pad button actions here?
numberButtons.forEach { b in
b.addTarget(self, action: #selector(self.numberPadButtonTapped(_:)), for: .touchUpInside)
}
}
#objc func numberPadButtonTapped(_ btn: UIButton) -> Void {
let t = btn.currentTitle ?? "Delete"
print("Tapped:", t)
// do what you want based on which button was tapped
}
private func initHeader()
{
// not clear how you're setting up your "header"
// so I'll guess at it
// view properties
headerView.backgroundColor = .black
headerView.clipsToBounds = true
headerView.layer.cornerRadius = 24
headerView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
// static text label
let transferToLabel = UILabel()
transferToLabel.text = "Transfer to..."
transferToLabel.font = UIFont.systemFont(ofSize: 14.0, weight: .regular)
// PayPal button
let payPalButton = UIButton()
payPalButton.setTitle("PayPal", for: [])
payPalButton.backgroundColor = .white
payPalButton.setTitleColor(.blue, for: [])
// data label fonts
currencyLabel.font = UIFont.systemFont(ofSize: 28.0, weight: .bold)
currValueLabel.font = currencyLabel.font
balanceLabel.font = UIFont.systemFont(ofSize: 11.0, weight: .regular)
// label text color
[transferToLabel, currencyLabel, currValueLabel, balanceLabel].forEach {
$0.textColor = .white
}
// horizontal stack to hold currencyLabel, currValueLabel
let currValStack = UIStackView()
currValStack.axis = .horizontal
currValStack.spacing = 12
// vertical stack to hold currValStack, balanceLabel
let innerVStack = UIStackView()
innerVStack.axis = .vertical
innerVStack.alignment = .center
innerVStack.spacing = 2
// add labels to Horizontal stack
currValStack.addArrangedSubview(currencyLabel)
currValStack.addArrangedSubview(currValueLabel)
// add horizontal stack and balanceLabel to vertical stack
innerVStack.addArrangedSubview(currValStack)
innerVStack.addArrangedSubview(balanceLabel)
// view to hold vertical stack (so we can center it vertically)
let innerView = UIView()
// add vertical stack to innerView
innerView.addSubview(innerVStack)
// add elements to headerView
headerView.addSubview(transferToLabel)
headerView.addSubview(payPalButton)
headerView.addSubview(innerView)
// we'll be applying constraints
[headerView, transferToLabel, payPalButton, currencyLabel, currValueLabel, balanceLabel,
innerView, innerVStack].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
NSLayoutConstraint.activate([
// "Transfer to..." label Top: Padding, leading: Padding
transferToLabel.topAnchor.constraint(equalTo: headerView.topAnchor, constant: Padding),
transferToLabel.leadingAnchor.constraint(equalTo: headerView.leadingAnchor, constant: Padding),
// payPal button Top: Padding to transfer label Bottom
// Leading / Trailing to Leading / Trailing with Padding
payPalButton.topAnchor.constraint(equalTo: transferToLabel.bottomAnchor, constant: Padding),
payPalButton.leadingAnchor.constraint(equalTo: headerView.leadingAnchor, constant: Padding),
payPalButton.trailingAnchor.constraint(equalTo: headerView.trailingAnchor, constant: -Padding),
// payPalButton explicit height
payPalButton.heightAnchor.constraint(equalToConstant: 50.0),
// innerView Top: 0 to payPal button Bottom
// Leading / Trailing to Leading / Trailing with Padding
// Bottom: 0
innerView.topAnchor.constraint(equalTo: payPalButton.bottomAnchor, constant: 0.0),
innerView.leadingAnchor.constraint(equalTo: headerView.leadingAnchor, constant: Padding),
innerView.trailingAnchor.constraint(equalTo: headerView.trailingAnchor, constant: -Padding),
innerView.bottomAnchor.constraint(equalTo: headerView.bottomAnchor, constant: 0.0),
// innerVStack Top / Bottom to innerView Top / Bottom with 24-pts padding
// centerX
innerVStack.topAnchor.constraint(equalTo: innerView.topAnchor, constant: 32.0),
innerVStack.bottomAnchor.constraint(equalTo: innerView.bottomAnchor, constant: -32.0),
innerVStack.centerXAnchor.constraint(equalTo: innerView.centerXAnchor, constant: 0.0),
])
}
private func initNumpad()
{
numberButtons.removeAll()
// numberPad stack properties
numberPadStackView.axis = .vertical
numberPadStackView.alignment = .fill
numberPadStackView.distribution = .fillEqually
numberPadStackView.spacing = 4
// a little more logical way to manage button layout
let buttonLabels: [[String]] = [
["1", "2", "3"],
["4", "5", "6"],
["7", "8", "9"],
[".", "0", "<"],
]
// adjust as desired
let btnFontSize: CGFloat = 28
buttonLabels.forEach { thisRowLabels in
// create a "row" stack view
let rowStack = UIStackView()
rowStack.axis = .horizontal
rowStack.alignment = .fill
rowStack.distribution = .fillEqually
// same horizontal spacing as "number pad" stack's vertical spacing
rowStack.spacing = numberPadStackView.spacing
// for each number string
thisRowLabels.forEach { s in
// create button
let btn = UIButton()
if s == "<" {
// if it's the "delete button"
// set image here
let iconCfg = UIImage.SymbolConfiguration(pointSize: btnFontSize, weight: .bold, scale: .large)
if let normIcon = UIImage(systemName: "delete.left", withConfiguration: iconCfg)?.withTintColor(.black, renderingMode: .alwaysOriginal),
let highIcon = UIImage(systemName: "delete.left", withConfiguration: iconCfg)?.withTintColor(.lightGray, renderingMode: .alwaysOriginal)
{
btn.setImage(normIcon, for: .normal)
btn.setImage(highIcon, for: .highlighted)
}
} else {
// set number pad button title
btn.setTitle(s, for: [])
}
// number pad button properties
btn.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
btn.setTitleColor(.black, for: .normal)
btn.setTitleColor(.lightGray, for: .highlighted)
btn.titleLabel?.font = UIFont.systemFont(ofSize: btnFontSize, weight: .bold)
// let's give 'em a rounded-corner border
btn.layer.borderColor = UIColor.blue.cgColor
btn.layer.borderWidth = 1
btn.layer.cornerRadius = 8
// allow buttons to stretch vertically!!!
btn.setContentHuggingPriority(UILayoutPriority(rawValue: 249), for: .vertical)
// add button to this row stack
rowStack.addArrangedSubview(btn)
// add button to numberButtons array
numberButtons.append(btn)
}
// add this rowStack to the number pad stack
numberPadStackView.addArrangedSubview(rowStack)
}
}
}
The result, on iPhone 8:
iPhone 11:
and iPhone 11 Pro Max:

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
}
}

swift how to set autolayout programmatically

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

viewDidLayoutSubviews() issue

I have the following screen, in order to centre the boxes between the uilabel and the keyboard I created a simple calculation where I added the header height then the uilabel height and then the keyboard height minus the whole screen size and divided everything by two in order to get the centre point [i added the code below although it's not relevant]
The issue is that in order to get the UILabels height I have to set the constraints in viewDidLayoutSubviews(). Now the idea is that every time you fill one box the next box becomes the first responder but that doesn't work because every time a box becomes a first responder viewDidLayoutSubviews() is being called.
What can I do about this ? is there a way to get the height of uiLabel without calling viewDidLayoutSubviews().
This is the code for centering the boxes :
override func viewDidLayoutSubviews() {
let allItems = headerC.frame.height + stackViewOfDefin.frame.height + 270 + 20
let getSpace = UIScreen.main.bounds.height - allItems
let stackview = UIStackView(arrangedSubviews: gameBoxes)
stackview.axis = .horizontal
stackview.spacing = 5
stackview.distribution = .fillEqually
stackview.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(stackview)
NSLayoutConstraint.activate([
stackview.topAnchor.constraint(equalTo: secondLine.bottomAnchor, constant: getSpace/2-30),
stackview.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
stackview.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: 10),
stackview.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -10)
])
}