Resize superview according to label's font size and image's aspect ratio - swift

This is an auto-layout related question. I've containerView which has two subviews: imageView and label. I want to let the fontsize of the label determine the size of the containerView according to the aspect ratio of imageView.
When the font size increases, the containerView and the imageView should get bigger maintaining the aspect ratio and keeping the label centered with some padding as shown in the image below.
And I want to achieve it programmatically.
Any help will be much appreciated

You can accomplish this by:
constrain image view to all 4 sides of container
constrain label centered in container
constrain image view to 16:9 ratio
constrain image view's height to label's height + desired "padding"
Here's an example, including buttons to increase / decrease the font size:
class WalterViewController: UIViewController {
let theContainerView: UIView = {
let v = UIView()
v.backgroundColor = .blue
return v
}()
let theImageView: UIImageView = {
let v = UIImageView()
v.backgroundColor = .red
v.contentMode = .scaleToFill
return v
}()
let theLabel: UILabel = {
let v = UILabel()
v.backgroundColor = .yellow
v.textAlignment = .center
v.text = "TEST"
// content vertical hugging REQUIRED !!!
v.setContentHuggingPriority(.required, for: .vertical)
return v
}()
let btnUp: UIButton = {
let b = UIButton(type: .system)
b.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
b.setTitle("Increase", for: .normal)
return b
}()
let btnDn: UIButton = {
let b = UIButton(type: .system)
b.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
b.setTitle("Decrease", for: .normal)
return b
}()
let btnStack: UIStackView = {
let v = UIStackView()
v.axis = .horizontal
v.spacing = 12
v.distribution = .fillEqually
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
// we'll be using constraints
[theContainerView, theImageView, theLabel, btnUp, btnDn, btnStack].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
// add buttons to the stack
btnStack.addArrangedSubview(btnUp)
btnStack.addArrangedSubview(btnDn)
// add imageView and label to container
theContainerView.addSubview(theImageView)
theContainerView.addSubview(theLabel)
// add button stack and container view to view
view.addSubview(btnStack)
view.addSubview(theContainerView)
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// horizontal button stack 20-points from top, 40-points on each side
btnStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
btnStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
btnStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
// container view centered in view safeArea
theContainerView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
theContainerView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
// constrain image view to its superView (container view)
// 8-pts on all 4 sides
theImageView.topAnchor.constraint(equalTo: theContainerView.topAnchor, constant: 8.0),
theImageView.leadingAnchor.constraint(equalTo: theContainerView.leadingAnchor, constant: 8.0),
theImageView.trailingAnchor.constraint(equalTo: theContainerView.trailingAnchor, constant: -8.0),
theImageView.bottomAnchor.constraint(equalTo: theContainerView.bottomAnchor, constant: -8.0),
// label is centered in its superView (container view)
theLabel.centerXAnchor.constraint(equalTo: theContainerView.centerXAnchor),
theLabel.centerYAnchor.constraint(equalTo: theContainerView.centerYAnchor),
// constrain imageView to 16:9 ratio
theImageView.widthAnchor.constraint(equalTo: theImageView.heightAnchor, multiplier: 16.0 / 9.0),
// constrain imageView's height to label's height +40
// will result in 20-pts on top and bottom
theImageView.heightAnchor.constraint(equalTo: theLabel.heightAnchor, constant: 40.0),
])
// load an image
if let img = UIImage(named: "bkg640x360") {
theImageView.image = img
}
// add targets to buttons to increase / decrease the label's font size
btnUp.addTarget(self, action: #selector(increaseTapped(_:)), for: .touchUpInside)
btnDn.addTarget(self, action: #selector(decreaseTapped(_:)), for: .touchUpInside)
}
#objc func increaseTapped(_ sender: Any?) -> Void {
theLabel.font = theLabel.font.withSize(theLabel.font.pointSize + 1.0)
}
#objc func decreaseTapped(_ sender: Any?) -> Void {
theLabel.font = theLabel.font.withSize(theLabel.font.pointSize - 1.0)
}
}
How it looks on launch (container view is centered in root view):
and, after tapping Increase a bunch of times:

Related

NSLayoutConstraints not cooperating with UIImageView

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

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

Button Constraints within a StackView (Swift Programmatically)

I'm new to setting up StackViews and Buttons programmatically. I am getting some strange behavior with my constraints I cannot figure out what I'm doing wrong. It feels like I'm missing something simple. Any help is greatly appreciated!
I am trying to add two buttons to a StackView to create a custom tab bar. However, when I add the constraints to the buttons they are showing up outside the bottom of StackView. It's like the top constraint of Earth image isn't working. Any ideas? See image and code below.
// View to put in the StackView
class ProfileBottomTabBarView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
self.translatesAutoresizingMaskIntoConstraints = false
self.backgroundColor = .blue
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// Calculate the screen height
public var screenHeight: CGFloat {
return UIScreen.main.bounds.height
}
// StackView height set to a proporation of screen height
let stackViewHeight = screenHeight * 0.07
// Views to put in the StackView
let profileIconView = ProfileBottomTabBarView()
let actIconView = ActBottomTabBarView()
let achieveIconView = AchieveBottomTabBarView()
let growIconView = GrowBottomTabBarView()
// Buttons to put in the Views
let profileButton = UIButton(type: .system)
let actButton = UIButton(type: .system)
let achieveButton = UIButton(type: .system)
let growButton = UIButton(type: .system)
let profileButtonText = UIButton(type: .system)
let actButtonText = UIButton(type: .system)
let achieveButtonText = UIButton(type: .system)
let growButtonText = UIButton(type: .system)
// Stackview setup
lazy var stackView: UIStackView = {
let stackV = UIStackView(arrangedSubviews: [profileIconView, actIconView, achieveIconView, growIconView])
stackV.translatesAutoresizingMaskIntoConstraints = false
stackV.axis = .horizontal
stackV.spacing = 20
stackV.distribution = .fillEqually
return stackV
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
// Add StackView
view.addSubview(stackView)
stackView.bottomAnchor.constraint(equalTo: view.safeBottomAnchor).isActive = true
stackView.leadingAnchor.constraint(equalTo: view.safeLeadingAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo: view.safeTrailingAnchor).isActive = true
// Set height of the bottom tab bar as a proportion of the screen height.
stackView.heightAnchor.constraint(equalToConstant: stackViewHeight).isActive = true
profileIconView.topAnchor.constraint(equalTo: stackView.topAnchor).isActive = true
profileIconView.bottomAnchor.constraint(equalTo: stackView.bottomAnchor).isActive = true
profileIconView.heightAnchor.constraint(equalToConstant: stackViewHeight).isActive = true
// Add Buttons to the View
profileIconView.addSubview(profileButton)
profileIconView.addSubview(profileButtonText)
profileButton.translatesAutoresizingMaskIntoConstraints = false
profileButtonText.translatesAutoresizingMaskIntoConstraints = false
// Profile Button with Earth Image Setup
profileButton.setImage(UIImage(named: "earthIcon"), for: .normal)
profileButton.imageView?.contentMode = .scaleAspectFit
profileButton.topAnchor.constraint(equalTo: profileIconView.topAnchor).isActive = true
profileButton.bottomAnchor.constraint(equalTo: profileButtonText.topAnchor).isActive = true
profileButton.centerXAnchor.constraint(equalTo: profileIconView.centerXAnchor).isActive = true
//Set height of icon to a proportion of the stackview height
let profileButtonHeight = stackViewHeight * 0.8
profileButton.heightAnchor.constraint(equalTo: profileIconView.heightAnchor, constant: profileButtonHeight).isActive = true
profileButton.widthAnchor.constraint(equalToConstant: profileButtonHeight).isActive = true
profileButton.imageView?.widthAnchor.constraint(equalToConstant: profileButtonHeight)
profileButton.imageView?.heightAnchor.constraint(equalToConstant: profileButtonHeight)
// Profile Text Button Setup
profileButtonText.setTitle("Profile", for: .normal)
profileButtonText.titleLabel?.font = UIFont.boldSystemFont(ofSize: 12)
profileButtonText.setTitleColor(.white, for: .normal)
profileButtonText.topAnchor.constraint(equalTo: profileButton.bottomAnchor).isActive = true
profileButtonText.bottomAnchor.constraint(equalTo: profileIconView.bottomAnchor).isActive = true
profileButtonText.centerXAnchor.constraint(equalTo: profileIconView.centerXAnchor).isActive = true
//Set height of icon to a proportion of the stackview height
let profileButtonTextHeight = stackViewHeight * 0.2
profileButton.heightAnchor.constraint(equalTo: profileIconView.heightAnchor, constant: profileButtonTextHeight).isActive = true
profileButtonText.widthAnchor.constraint(equalToConstant: 40).isActive = true
}
A few things wrong with your constraints...
You're calculating heights / widths and using them as constants, but those values may (almost certainly will) change based on view lifecycle.
Better to use only related constraints. For example:
// constrain profile image button top, centerX and width relative to the iconView
profileButton.topAnchor.constraint(equalTo: profileIconView.topAnchor),
profileButton.centerXAnchor.constraint(equalTo: profileIconView.centerXAnchor),
profileButton.widthAnchor.constraint(equalTo: profileIconView.widthAnchor, multiplier: 1.0),
// constrain profile text button bottom, centerX and width relative to the iconView
profileButtonText.centerXAnchor.constraint(equalTo: profileIconView.centerXAnchor),
profileButtonText.widthAnchor.constraint(equalTo: profileIconView.widthAnchor, multiplier: 1.0),
profileButtonText.bottomAnchor.constraint(equalTo: profileIconView.bottomAnchor),
// constrain bottom of image button to top of text button (with a padding of 4-pts, change to suit)
profileButton.bottomAnchor.constraint(equalTo: profileButtonText.topAnchor, constant: -4.0),
// constrain height of text button to 20% of height of iconView
profileButtonText.heightAnchor.constraint(equalTo: profileIconView.heightAnchor, multiplier: 0.2),
To make things easier on yourself, I'd suggest creating a BottomTabBarView that handles adding and constraining your buttons:
class BottomTabBarView: UIView {
var theImageButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.imageView?.contentMode = .scaleAspectFit
return v
}()
var theTextButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.titleLabel?.font = UIFont.boldSystemFont(ofSize: 12)
v.setTitleColor(.white, for: .normal)
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
convenience init(withImageName imageName: String, labelText: String, bkgColor: UIColor) {
self.init()
self.commonInit()
theImageButton.setImage(UIImage(named: imageName), for: .normal)
theTextButton.setTitle(labelText, for: .normal)
backgroundColor = bkgColor
}
func commonInit() -> Void {
self.translatesAutoresizingMaskIntoConstraints = false
addSubview(theImageButton)
addSubview(theTextButton)
NSLayoutConstraint.activate([
// constrain profile image button top, centerX and width of the iconView
theImageButton.topAnchor.constraint(equalTo: topAnchor),
theImageButton.centerXAnchor.constraint(equalTo: centerXAnchor),
theImageButton.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 1.0),
// constrain profile text button bottom, centerX and width of the iconView
theTextButton.centerXAnchor.constraint(equalTo: centerXAnchor),
theTextButton.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 1.0),
theTextButton.bottomAnchor.constraint(equalTo: bottomAnchor),
// constrain bottom of image button to top of text button
theImageButton.bottomAnchor.constraint(equalTo: theTextButton.topAnchor, constant: -4.0),
// set text button height to 20% of view height (instead of using intrinsic height)
theTextButton.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.2),
])
}
}
Now you can create each view with a single line, as in:
profileIconView = BottomTabBarView(withImageName: "earthIcon", labelText: "Profile", bkgColor: .blue)
And your view controller class becomes much simpler / cleaner:
class BenViewController: UIViewController {
// Views to put in the StackView
var profileIconView = BottomTabBarView()
var actIconView = BottomTabBarView()
var achieveIconView = BottomTabBarView()
var growIconView = BottomTabBarView()
// Stackview setup
lazy var stackView: UIStackView = {
let stackV = UIStackView(arrangedSubviews: [profileIconView, actIconView, achieveIconView, growIconView])
stackV.translatesAutoresizingMaskIntoConstraints = false
stackV.axis = .horizontal
stackV.spacing = 20
stackV.distribution = .fillEqually
return stackV
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
profileIconView = BottomTabBarView(withImageName: "earthIcon", labelText: "Profile", bkgColor: .blue)
actIconView = BottomTabBarView(withImageName: "actIcon", labelText: "Action", bkgColor: .brown)
achieveIconView = BottomTabBarView(withImageName: "achieveIcon", labelText: "Achieve", bkgColor: .red)
growIconView = BottomTabBarView(withImageName: "growIcon", labelText: "Grow", bkgColor: .purple)
// Add StackView
view.addSubview(stackView)
NSLayoutConstraint.activate([
// constrain stackView to bottom, leading and trailing (to safeArea)
stackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
// Set height of the stackView (the bottom tab bar) as a proportion of the view height (7%).
stackView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.07),
])
}
}

UIView resize to fit labels inside of it

I have a UIView that I'm appending into a stack view on my main page of my app
This is the class of my view:
class MyCustomView: UIView {
public let leftLabel: UILabel = UILabel(frame: .zero)
public let rightLabel: UILabel = UILabel(frame: .zero)
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(leftLabel)
addSubview(rightLabel)
leftLabel.translatesAutoresizingMaskIntoConstraints = false
rightLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
leftLabel.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.4),
leftLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
leftLabel.topAnchor.constraint(equalTo: topAnchor),
leftLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
rightLabel.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.6),
rightLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
rightLabel.topAnchor.constraint(equalTo: topAnchor),
rightLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
])
leftLabel.text = "Short string"
rightLabel.text = "Short string too"
}
}
And I append to my main stack view with:
let myCustomView = MyCustomView(frame: .zero)
stackView.addArrangedSubview(myCustomView)
This loads in my label's correctly and resizes everything as I'd want.
However, in my main class, I am updating myCustomView.rightLabel.text = <New Way Longer Text That Takes 2 Lines Instead of One>
The text is updating properly, but my myCustomView size is not resizing, so part of the text is just being cut-off
I have tried following other answers on here, but none of them seem to work for me.
Am I missing something small in order to force the resize of the customView to fit the label inside of it?
Thank you in advance
Your code does not show that you set the .numberOfLines in the label(s) to 0 to allow for multi-line labels.
Adding only that, should allow your labels to grow in height and to expand your custom view. However... that will also make both labels expand to the size of the tallest label, resulting in the text of the "shorter" label being vertically centered (I added background colors to make it easy to see the frames / bounds of the views):
If you constrain the Bottom of your custom view to the Bottom of each label at greaterThanOrEqualTo you can keep the labels "top-aligned":
You can run this code directly in a Playground page to see the results:
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class MyCustomView: UIView {
public let leftLabel: UILabel = UILabel(frame: .zero)
public let rightLabel: UILabel = UILabel(frame: .zero)
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(leftLabel)
addSubview(rightLabel)
// background colors so we can see the view frames
backgroundColor = .cyan
leftLabel.backgroundColor = .yellow
rightLabel.backgroundColor = .green
// we want multi-line labels
leftLabel.numberOfLines = 0
rightLabel.numberOfLines = 0
// use auto-layout
leftLabel.translatesAutoresizingMaskIntoConstraints = false
rightLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
// constrain to top
leftLabel.topAnchor.constraint(equalTo: topAnchor),
// constrain to left
leftLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
// constrain width = 40%
leftLabel.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.4),
// constrain to top
rightLabel.topAnchor.constraint(equalTo: topAnchor),
// constrain to right
rightLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
// constrain width = 60%
rightLabel.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.6),
// constrain bottom of view (self) to >= 0 from bottom of leftLabel
bottomAnchor.constraint(greaterThanOrEqualTo: leftLabel.bottomAnchor, constant: 0.0),
// constrain bottom of view (self) to >= 0 from bottom of rightLabel
bottomAnchor.constraint(greaterThanOrEqualTo: rightLabel.bottomAnchor, constant: 0.0),
])
leftLabel.text = "Short string"
rightLabel.text = "Short string too"
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class MyViewController : UIViewController {
var theButton: UIButton = {
let b = UIButton()
b.setTitle("Tap Me", for: .normal)
b.translatesAutoresizingMaskIntoConstraints = false
b.backgroundColor = .red
return b
}()
var theStackView: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .vertical
v.spacing = 8
v.distribution = .equalSpacing
return v
}()
var myView = MyCustomView()
// on button tap, change the text in the label(s)
#objc func didTap(_ sender: Any?) -> Void {
myView.leftLabel.text = "Short string with\nA\nB\nC\nD\nE"
myView.rightLabel.text = "Short string too\nA\nB"
}
override func loadView() {
let view = UIView()
self.view = view
view.backgroundColor = .white
view.addSubview(theButton)
// constrain button to Top: 32 and centerX
NSLayoutConstraint.activate([
theButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 32.0),
theButton.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0.0),
])
view.addSubview(theStackView)
// constrain stack view to Top: 100 and Leading/Trailing" 0
// no Bottom or Height constraint
NSLayoutConstraint.activate([
theStackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 100.0),
theStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0.0),
theStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0.0),
])
theStackView.addArrangedSubview(myView)
// add an action for the button tap
theButton.addTarget(self, action: #selector(didTap(_:)), for: .touchUpInside)
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()