More complicated AutoLayout equations - swift

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.

Related

Swift, Scroll View, Stack View, programatical Button Creation and Auto Layout causes weird Behaviors / Errors

I have a simple iOS App, containing a Scroll View (scroll horizontally, but mainly as described here: Is it possible for UIStackView to scroll?). By default the Horizontal Stack is empty (spacing = Standard, Distribution is Fill, but also Equal Spacing does not improve this), and I add buttons programatically to it (for example purpose I add a random length string, to have various sizes), using:
filterStackView.translatesAutoresizingMaskIntoConstraints = false
let newButtonConfiguration = UIButton.Configuration.filled()
let newButton = UIButton(configuration: newButtonConfiguration)
newButton.translatesAutoresizingMaskIntoConstraints = false
newButton.setTitle("\(randomString(length: Int.random(in: 5..<25)))", for: .normal)
newButton.setImage(UIImage(systemName: "xmark.circle", withConfiguration: UIImage.SymbolConfiguration(scale: .unspecified)), for: .normal)
newButton.addTarget(self, action: #selector(newButtonPressedAction), for: .touchUpInside)
filterStackView.addArrangedSubview(newButton)
I already tried adding to the button:
newButton.sizeToFit()
newButton.layoutIfNeeded()
or to the View Stack:
filterStackView.sizeToFit()
filterStackView.layoutIfNeeded()
Without any visible change. The weird things happening are:
After adding another one, sizes start changing in a funny way (if buttons are added from Story Board they really respect the width of the text - and Autolayout works fine):
And after playing with adding and removing them for a while:
And in the debug console I see constraints failing badly:
2022-11-01 14:05:48.450435+0100 TestAppSideScroll[20626:640959] [LayoutConstraints] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(
"<NSLayoutConstraint:0x600001b58c80 'UISV-canvas-connection' H:[UIButton:0x122408660'N6xoAsl']-(0)-| (active, names: '|':UIStackView:0x1505080c0 )>",
"<_UISystemBaselineConstraint:0x600001b586e0 'UISV-spacing' H:[UIButton:0x1223095d0'oHSryjPsjrZA']-(NSLayoutAnchorConstraintSpace(8))-[UIButton:0x122408660'N6xoAsl'] (active)>"
)
Will attempt to recover by breaking constraint
<_UISystemBaselineConstraint:0x600001b586e0 'UISV-spacing' H:[UIButton:0x1223095d0'oHSryjPsjrZA']-(NSLayoutAnchorConstraintSpace(8))-[UIButton:0x122408660'N6xoAsl'] (active)>
Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful.
Which actually makes no sense, since the conflict seems to be the spacing constraint with the connection to the canvas constraint? But that I would expect to happen for every single button I add, if that would be the general problem - however there are no real constraints, except the spacing and the snapping of the scroll view.
Any help would be appreciated.
Yeah... something really weird going on there.
My guess would be that UIKit has some sort of algorithm that is trying to apply "readability" to the buttons. Clearly, though, that's not the goal here.
After some quick testing, one way to get around this is to add the button as a subview to a clear "container" view, and then add that container view to the stack view.
Some sample code:
class SampleViewController: UIViewController {
let filterStackViewA: UIStackView = {
let v = UIStackView()
v.spacing = 8
v.distribution = .fill
v.alignment = .center
return v
}()
let filterStackViewB: UIStackView = {
let v = UIStackView()
v.spacing = 8
v.distribution = .fill
v.alignment = .center
return v
}()
let scrollViewA: UIScrollView = {
let v = UIScrollView()
v.backgroundColor = .systemYellow
return v
}()
let scrollViewB: UIScrollView = {
let v = UIScrollView()
v.backgroundColor = .systemOrange
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .darkGray
let addBtn = UIButton()
addBtn.setTitle("Add Button", for: [])
addBtn.setTitleColor(.white, for: .normal)
addBtn.setTitleColor(.lightGray, for: .highlighted)
addBtn.backgroundColor = .systemGreen
addBtn.layer.cornerRadius = 8
let labelA: UILabel = {
let v = UILabel()
v.textColor = .white
v.font = .systemFont(ofSize: 15.0, weight: .light)
v.text = "Buttons added to \"Container\" views..."
return v
}()
let labelB: UILabel = {
let v = UILabel()
v.textColor = .white
v.font = .systemFont(ofSize: 15.0, weight: .light)
v.text = "Buttons added directly to stack view..."
return v
}()
[addBtn, scrollViewA, scrollViewB, filterStackViewA, filterStackViewB, labelA, labelB].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
view.addSubview(addBtn)
view.addSubview(labelA)
view.addSubview(scrollViewA)
scrollViewA.addSubview(filterStackViewA)
view.addSubview(labelB)
view.addSubview(scrollViewB)
scrollViewB.addSubview(filterStackViewB)
let g = view.safeAreaLayoutGuide
let cgA = scrollViewA.contentLayoutGuide
let fgA = scrollViewA.frameLayoutGuide
let cgB = scrollViewB.contentLayoutGuide
let fgB = scrollViewB.frameLayoutGuide
NSLayoutConstraint.activate([
addBtn.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
addBtn.widthAnchor.constraint(equalToConstant: 200.0),
addBtn.centerXAnchor.constraint(equalTo: g.centerXAnchor),
labelA.topAnchor.constraint(equalTo: addBtn.bottomAnchor, constant: 40.0),
labelA.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
labelA.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollViewA.topAnchor.constraint(equalTo: labelA.bottomAnchor, constant: 8.0),
scrollViewA.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
scrollViewA.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollViewA.heightAnchor.constraint(equalToConstant: 60.0),
filterStackViewA.topAnchor.constraint(equalTo: cgA.topAnchor, constant: 4.0),
filterStackViewA.leadingAnchor.constraint(equalTo: cgA.leadingAnchor, constant: 8.0),
filterStackViewA.trailingAnchor.constraint(equalTo: cgA.trailingAnchor, constant: -8.0),
filterStackViewA.bottomAnchor.constraint(equalTo: cgA.bottomAnchor, constant: -4.0),
filterStackViewA.heightAnchor.constraint(equalTo: fgA.heightAnchor, constant: -8.0),
labelB.topAnchor.constraint(equalTo: filterStackViewA.bottomAnchor, constant: 20.0),
labelB.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
labelB.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollViewB.topAnchor.constraint(equalTo: labelB.bottomAnchor, constant: 8.0),
scrollViewB.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
scrollViewB.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollViewB.heightAnchor.constraint(equalToConstant: 60.0),
filterStackViewB.topAnchor.constraint(equalTo: cgB.topAnchor, constant: 4.0),
filterStackViewB.leadingAnchor.constraint(equalTo: cgB.leadingAnchor, constant: 8.0),
filterStackViewB.trailingAnchor.constraint(equalTo: cgB.trailingAnchor, constant: -8.0),
filterStackViewB.bottomAnchor.constraint(equalTo: cgB.bottomAnchor, constant: -4.0),
filterStackViewB.heightAnchor.constraint(equalTo: fgB.heightAnchor, constant: -8.0),
])
addBtn.addTarget(self, action: #selector(addButton), for: .touchUpInside)
}
#objc func addButton() {
// unwrap optional system image
guard let img = UIImage(systemName: "xmark.circle", withConfiguration: UIImage.SymbolConfiguration(scale: .unspecified))
else { return }
var newButtonConfiguration = UIButton.Configuration.filled()
newButtonConfiguration.title = randomString(length: Int.random(in: 5..<15))
newButtonConfiguration.image = img
// let's add a new button to a "container" view
let newButtonA = UIButton(configuration: newButtonConfiguration)
newButtonA.translatesAutoresizingMaskIntoConstraints = false
let v = UIView()
v.addSubview(newButtonA)
NSLayoutConstraint.activate([
newButtonA.topAnchor.constraint(equalTo: v.topAnchor),
newButtonA.leadingAnchor.constraint(equalTo: v.leadingAnchor),
newButtonA.trailingAnchor.constraint(equalTo: v.trailingAnchor),
newButtonA.bottomAnchor.constraint(equalTo: v.bottomAnchor),
])
// add the container view to the stack view
filterStackViewA.addArrangedSubview(v)
// let's add a new button directly to the stack view
let newButtonB = UIButton(configuration: newButtonConfiguration)
filterStackViewB.addArrangedSubview(newButtonB)
newButtonA.addTarget(self, action: #selector(removeMe(_:)), for: .touchUpInside)
newButtonB.addTarget(self, action: #selector(removeMe(_:)), for: .touchUpInside)
// let's make sure the new button is visible
// note: this is just for example...
// if we rapidly tap and add buttons, this can easily "miss" the last button
// so don't expect this to be "production" code
DispatchQueue.main.async {
let sz = self.scrollViewA.contentSize
let rA = CGRect(x: sz.width - 1.0, y: 0.0, width: 1.0, height: 1.0)
self.scrollViewA.scrollRectToVisible(rA, animated: true)
let szB = self.scrollViewB.contentSize
let rB = CGRect(x: szB.width - 1.0, y: 0.0, width: 1.0, height: 1.0)
self.scrollViewB.scrollRectToVisible(rB, animated: true)
}
}
#objc func removeMe(_ sender: UIButton) {
// get a reference to the tapped button's superview
guard let sv = sender.superview else { return }
var v: UIView!
if sv is UIStackView {
// we tapped a button added directly to the stack view
v = sender
} else {
// we tapped a button that's in a "container" view
v = sv
}
guard let st = v.superview as? UIStackView,
let idx = st.arrangedSubviews.firstIndex(of: v),
filterStackViewA.arrangedSubviews.count > idx,
filterStackViewB.arrangedSubviews.count > idx
else {
print("something's not setup right, so return")
return
}
let bA = filterStackViewA.arrangedSubviews[idx]
let bB = filterStackViewB.arrangedSubviews[idx]
// let's animate the buttons away
bA.isHidden = true
bB.isHidden = true
UIView.animate(withDuration: 0.5, animations: {
self.view.layoutIfNeeded()
}, completion: { _ in
bA.removeFromSuperview()
bB.removeFromSuperview()
})
}
func randomString(length: Int) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String((0..<length).map{ _ in letters.randomElement()! })
}
}
It will look like this when running:
Each tap of "Add Button" will add a new button to each scrolling-stackView, with the top set using buttons in container views. Tapping any of those buttons will remove it from its stack view, along with its "twin" from the other one.

How to resize content view in UIScrollView programmatically?

Here is the issue: I need to add a view containing other subviews to a scroll view programmatically. In addition, I also need to make the frame of such a view to stick to the bounds of the main super view. The code below shows the approach I was trying to implement, but as you can see from the pictures below the 'contentView' is not updating its frame size when the screen is rotated. The initial code is taken from here for demonstration purposes. Any help would be greatly appreciated.
import UIKit
class TestViewController : UIViewController {
var contentViewSize = CGSize()
let contentView: UIView = {
let view = UIView()
view.backgroundColor = .magenta
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
let labelOne: UILabel = {
let label = UILabel()
label.text = "Scroll Top"
label.backgroundColor = .red
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let labelTwo: UILabel = {
let label = UILabel()
label.text = "Scroll Bottom"
label.backgroundColor = .green
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let scrollView: UIScrollView = {
let v = UIScrollView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .cyan
return v
}()
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
contentViewSize = view.bounds.size
labelTwo.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: (contentViewSize.width - labelTwo.frame.size.width - 16.0)).isActive = true
labelTwo.topAnchor.constraint(equalTo: contentView.topAnchor, constant: (contentViewSize.height - labelTwo.frame.size.height - 16.0)).isActive = true
}
override func viewDidLoad() {
super.viewDidLoad()
contentViewSize = view.bounds.size
view.backgroundColor = .yellow
self.view.addSubview(scrollView)
scrollView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 8.0).isActive = true
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 8.0).isActive = true
scrollView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -8.0).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8.0).isActive = true
scrollView.addSubview(contentView)
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 16).isActive = true
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 16).isActive = true
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 16).isActive = true
contentView.rightAnchor.constraint(equalTo: scrollView.rightAnchor, constant: 16).isActive = true
contentView.addSubview(labelOne)
labelOne.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16.0).isActive = true
labelOne.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16.0).isActive = true
contentView.addSubview(labelTwo)
labelTwo.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: (contentViewSize.width - labelTwo.frame.size.width - 16.0)).isActive = true
labelTwo.topAnchor.constraint(equalTo: contentView.topAnchor, constant: (contentViewSize.height - labelTwo.frame.size.height - 16.0)).isActive = true
labelTwo.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16.0).isActive = true
labelTwo.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16.0).isActive = true
}
}
You made some odd changes to the code from the answer you linked to. Also, that answer is a little out-of-date.
Here's a better example. Assuming you want only vertical scrolling, this will:
add a Cyan scroll view, inset 8-pts on each side from the safe-area
add a Magenta "content view" to the scroll view, with 16-pts on each side constrained to the scroll view's contentLayoutGuide, with a width 32-pts less than the scroll view's frame (16-pts on each side)
add a label at top-left of the content view
add a label at bottom-right of the content view
constrain the bottom label 1500-pts below the top label (so it will scroll vertically)
Code:
class ScrollTestViewController : UIViewController {
let contentView: UIView = {
let view = UIView()
view.backgroundColor = .magenta
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
let labelOne: UILabel = {
let label = UILabel()
label.text = "Scroll Top"
label.backgroundColor = .red
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let labelTwo: UILabel = {
let label = UILabel()
label.text = "Scroll Bottom"
label.backgroundColor = .green
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let scrollView: UIScrollView = {
let v = UIScrollView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .cyan
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .yellow
// add the scroll view
self.view.addSubview(scrollView)
// add contentView to scroll view
scrollView.addSubview(contentView)
// add two labels to contentView
contentView.addSubview(labelOne)
contentView.addSubview(labelTwo)
// respect safe-area
let g = view.safeAreaLayoutGuide
// if you want to ignore the safe-area (bad idea),
// use this instead
//let g = view!
//scrollView.contentInsetAdjustmentBehavior = .never
// we're going to constrain the contentView to the scroll view's content layout guide
let scg = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
// constrain scrollView Top / Leading / Trailing / Bottom to view (safe-area)
// with 8-pts on each side
scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 8.0),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -8.0),
// constrain contentView Top / Leading / Trailing / Bottom to scroll view's Content Layout Guide
// with 16-pts on each side
contentView.topAnchor.constraint(equalTo: scg.topAnchor, constant: 16.0),
contentView.leadingAnchor.constraint(equalTo: scg.leadingAnchor, constant: 16.0),
contentView.trailingAnchor.constraint(equalTo: scg.trailingAnchor, constant: -16.0),
contentView.bottomAnchor.constraint(equalTo: scg.bottomAnchor, constant: -16.0),
// if we only want vertical scrolling, constrain contentView Width
// to scrollView's Frame Layout Guide minus 32-pts (because we have 16-pts on each side)
contentView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -32.0),
// constrain labelOne Top / Leading 16-pts to contentView Top / Leading
// (so it shows up at top-left corner)
labelOne.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16.0),
labelOne.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16.0),
// constrain labelTwo Bottom / Trailing 16-pts to contentView Bottom / Trailing
// (so it shows up at bottom-right corner)
labelTwo.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16.0),
labelTwo.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16.0),
// constrain labelTwo Top to labelOne Bottom + 1500-pts
// so we'll have some vertical scrolling to get to it
labelTwo.topAnchor.constraint(equalTo: labelOne.bottomAnchor, constant: 1500.0),
])
}
}

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:

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

Altered column width in horizontal UIStackview

I have a 4 x 1 UILabel matrix in a horizontal stackview I would like columns 1 and 3 to be the same width BUT narrower than columns 2 and 4. I have tried .fillproportionally, and others. I understood that this was possible... however, a solution evades me and Google hasn't turned anything up. Any help or a point in the right direction would be helpful. Thank you. I am not using Storyboards.
You can create your grid:
as you described:
C1-w == 1/3 of C2-w
C3-w == C1-w
C4-w == C2-w
pretty much with that description:
with the stack view .distribution = .fill
Here's a complete example:
class ColumnStackViewController: UIViewController {
let outerStack: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .vertical
v.alignment = .fill
v.distribution = .fill
v.spacing = 8
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
// create "header row" stack view
let headerRowStack = UIStackView()
headerRowStack.axis = .horizontal
headerRowStack.spacing = 4
// create 4 labels for header row
let headerC1 = UILabel()
let headerC2 = UILabel()
let headerC3 = UILabel()
let headerC4 = UILabel()
headerC1.text = "C1"
headerC2.text = "C2"
headerC3.text = "C3"
headerC4.text = "C4"
[headerC1, headerC2, headerC3, headerC4].forEach { v in
// give each "header" label a background color, so we can see the frames
v.backgroundColor = .yellow
v.textAlignment = .center
headerRowStack.addArrangedSubview(v)
}
NSLayoutConstraint.activate([
// constrain c1 width to 1/3 of c2 width
headerC1.widthAnchor.constraint(equalTo: headerC2.widthAnchor, multiplier: 1.0 / 3.0),
// constrain c3 width equal to c1 width
headerC3.widthAnchor.constraint(equalTo: headerC1.widthAnchor),
// constrain c4 width equal to c2 width
headerC4.widthAnchor.constraint(equalTo: headerC2.widthAnchor),
])
outerStack.addArrangedSubview(headerRowStack)
let c1Vals = ["HOLDING", "BOOK", "P/L"]
let c3Vals = ["PAID", "MARKET", "WEIGHT"]
for i in 0..<c1Vals.count {
// create a "row" stack view
let rowStack = UIStackView()
rowStack.axis = .horizontal
rowStack.spacing = headerRowStack.spacing
// add it to outer stack view
outerStack.addArrangedSubview(rowStack)
let rowC1 = UILabel()
let rowC2 = UILabel()
let rowC3 = UILabel()
let rowC4 = UILabel()
rowC1.text = c1Vals[i]
rowC3.text = c3Vals[i]
rowC2.text = "row \(i+1) c2 data"
rowC4.text = "row \(i+1) c4 data"
[rowC1, rowC2, rowC3, rowC4].forEach { v in
// give each row-label a border
v.layer.borderColor = UIColor.red.cgColor
v.layer.borderWidth = 0.5
// set the font
v.font = UIFont.systemFont(ofSize: 14.0, weight: .light)
// add it to the row stack view
rowStack.addArrangedSubview(v)
}
// constrain each row-label width to the header-row label width
for (hLabel, rowLabel) in zip([headerC1, headerC2, headerC3, headerC4], [rowC1, rowC2, rowC3, rowC4]) {
rowLabel.widthAnchor.constraint(equalTo: hLabel.widthAnchor).isActive = true
}
}
view.addSubview(outerStack)
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain outer stack view to Top / Leading / Trailing with 20-pt "padding"
outerStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
outerStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
outerStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
}
}
which produces this result:
Note: you'll have to use a pretty small font to fit that layout on a small device:
This is all i ended needing v1.widthAnchor.constraint(equalTo: v2.widthAnchor, multiplier: 0.5, constant: 0).isActive = true v3.widthAnchor.constraint(equalTo: v1.widthAnchor ).isActive = true v4.widthAnchor.constraint(equalTo: v2.widthAnchor ).isActive = true