UIStackView, arrange subviews only while they fit - ios

I have an array of UIButtons that I am trying to add to an horizontal UIStackView. The number of these buttons is dynamic and they can become that high to not fit on the screen.
For my use case I would like to add the buttons, from the left of my stack view, while they fit on the screen. So if I have 10 buttons, and after adding the 5th one there would not be enough space to add the 6th button, I would like to stop at that moment.
Does anybody knows a good approach to get this behaviour?
My current code looks similar to this:
import UIKit
class ViewController: UIViewController {
let items = ["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven"]
override func viewDidLoad() {
super.viewDidLoad()
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.spacing = 5
view.addSubview(stackView)
for title in items {
let button = UIButton()
button.backgroundColor = .red
button.setTitle(title, for: .normal)
// if button.doesNotFit() {
// break
// }
stackView.addArrangedSubview(button)
}
let spacerView = UIView()
spacerView.setContentHuggingPriority(.defaultLow, for: .horizontal)
stackView.addArrangedSubview(spacerView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
stackView.heightAnchor.constraint(equalToConstant: 50).isActive = true
}
}
With that, i get the following result. The problem is in the button "Nine", "Ten", etc... because they do not fit. I would not like them to be added to the UIStackView.

What you'll need to do...
in viewDidLoad()
create the buttons and add them to an array
in viewDidLayoutSubviews() (so we have a valid width of the view)
get the available width
clear any existing buttons from the stack view
loop through the buttons array, adding the width of the button + spacing
if the width is less than available width, add the button to the stack view
Here is a full example. I included .left, .right and .justified options:
// ButtonsAlignment is
// left = buttons will be left-aligned with fixed spacing
// right = buttons will be right-aligned with fixed spacing
// justified = buttons will be justified edge to edge with equal spacing
enum ButtonsAlignment {
case left
case right
case justified
}
class StackSpacingViewController: UIViewController {
let items = ["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven"]
// array to hold your buttons
var buttonsArray: [UIButton] = []
// change to .left, .right, .justified to see the difference
let spacingMode: ButtonsAlignment = .left
// if mode is .justified, this will be the minimum spacing
let buttonSpacing: CGFloat = 5.0
// need to track when the view width changes (when auto-layout has set it)
// for use in viewDidLayoutSubviews()
var viewWidth: CGFloat = 0.0
// we'll need to refrence the stack view in multiple places,
// so don't make it local to viewDidLoad()
let stackView = UIStackView()
override func viewDidLoad() {
super.viewDidLoad()
stackView.axis = .horizontal
if spacingMode == .left || spacingMode == .right {
stackView.distribution = .fill
stackView.spacing = buttonSpacing
} else {
stackView.distribution = .equalSpacing
}
view.addSubview(stackView)
for title in items {
let button = UIButton()
button.backgroundColor = .red
button.setTitle(title, for: .normal)
// append it to the buttonsArray
buttonsArray.append(button)
}
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
stackView.heightAnchor.constraint(equalToConstant: 50).isActive = true
if spacingMode == .left || spacingMode == .justified {
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
}
if spacingMode == .right || spacingMode == .justified {
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// this can (likely will) be called multiple times
// so only update the buttons stack view when the
// view's width changes
if viewWidth != view.frame.width {
let w = view.frame.width
viewWidth = w
// clear any buttons already in the stack
stackView.subviews.forEach {
$0.removeFromSuperview()
}
var curWidth: CGFloat = 0.0
for btn in buttonsArray {
curWidth += btn.intrinsicContentSize.width + buttonSpacing
if curWidth < w {
stackView.addArrangedSubview(btn)
}
}
stackView.setNeedsLayout()
stackView.layoutIfNeeded()
}
}
}
Note: this could (probably would) work better as a self-contained custom view class... would make it easy to add a "background" color, and the calculations could be handled in the subclass's layoutSubviews() function.

Related

How to center multiple UI elements within a parent UIView

I'm trying to create these 6 UITextField centered within a UIVIew that I have centered and is 0.85 the width of self.view. I was able to get it working on one iPhone size however it was hardcoded and doesn't transform well on other iPhone sizes.
So now I'm trying to figure out the best way to properly center these 6 elements.
Here's what I currently have:
class FormView: UIViewController {
//INSTANTIATING VARIABLES
...
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
setupCodeFieldView()
}
private func setupCodeFieldView(){
codeFieldView.backgroundColor = .green
codeFieldView.translatesAutoresizingMaskIntoConstraints = false
parentSubview.addSubview(codeFieldView)
codeFieldView.heightAnchor.constraint(equalToConstant: 60).isActive = true
codeFieldView.topAnchor.constraint(equalTo: parentSubview.bottomAnchor, constant: 5).isActive = true
//SETTING WIDTH SIZE AND CENTERING PARENT VIEW
codeFieldView.widthAnchor.constraint(equalTo:view.widthAnchor, multiplier: 0.85).isActive = true
codeFieldView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
setupCodeFields()
}
fileprivate func setupCodeFields() {
var textFieldArr: [UITextField] = []
for index in 0...5{
let field: UITextField = UITextField()
field.returnKeyType = .next
field.setUnderline()
field.borderStyle = .none
field.keyboardType = .numberPad
field.backgroundColor = UIColor.white
field.tag = index
field.textAlignment = .center
let valueWidth = codeFieldView.bounds.size.width/6
field.placeholder = String(describing: valueWidth)
field.accessibilityIdentifier = "field" + String(index)
codeFieldView.addSubview(field)
field.layer.cornerRadius = 5.0
field.layer.borderWidth = 1.0
field.layer.borderColor = UIColor(red: 0.45, green: 0.46, blue: 0.50, alpha: 1.00).cgColor
field.translatesAutoresizingMaskIntoConstraints = false
//HERE IS HOW SET THE WIDTH OF THE BUTTON
field.widthAnchor.constraint(equalToConstant: floor(valueWidth)-5).isActive = true
field.heightAnchor.constraint(equalToConstant: 60).isActive = true
field.topAnchor.constraint(equalTo: codeFieldView.topAnchor).isActive = true
if index == 0 {
field.leftAnchor.constraint(equalTo:codeFieldView.leftAnchor).isActive = true
} else {
field.leftAnchor.constraint(equalTo: textFieldArr[index-1].rightAnchor, constant: 5).isActive = true
}
textFieldArr.append(field)
}
}
}
Here's what I currently have. You can see that the 6 elements' parent view is centered and that I'm struggling to have the 6 children UITextFields perfectly spaced across the highlighted green parent view.
Mockup of how I'd like my UI to look:
You can do this easily with a UIStackView
Here's a quick example (based on your code):
class FormView: UIViewController {
//INSTANTIATING VARIABLES
//...
let codeFieldView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .gray
setupCodeFieldView()
}
private func setupCodeFieldView() {
codeFieldView.backgroundColor = .green
codeFieldView.translatesAutoresizingMaskIntoConstraints = false
// not clear what you're doing with "parentSubview"
// so let's just add it to the root view
view.addSubview(codeFieldView)
// always respect safe-area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// height of 60
codeFieldView.heightAnchor.constraint(equalToConstant: 60),
// 85% of the width
codeFieldView.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.85),
// centered vertically and horizontally
codeFieldView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
codeFieldView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
])
setupCodeFields()
}
fileprivate func setupCodeFields() {
// let's add a stack view to codeFieldView
let stackView = UIStackView()
stackView.spacing = 5
stackView.distribution = .fillEqually
stackView.translatesAutoresizingMaskIntoConstraints = false
codeFieldView.addSubview(stackView)
NSLayoutConstraint.activate([
// constrain stack view to all 4 sides of code field view
stackView.topAnchor.constraint(equalTo: codeFieldView.topAnchor),
stackView.leadingAnchor.constraint(equalTo: codeFieldView.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: codeFieldView.trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: codeFieldView.bottomAnchor),
])
// now we add the text fields
for index in 0...5 {
let field: UITextField = UITextField()
field.returnKeyType = .next
//field.setUnderline()
field.borderStyle = .none
field.keyboardType = .numberPad
field.backgroundColor = UIColor.white
field.tag = index
field.textAlignment = .center
field.accessibilityIdentifier = "field" + String(index)
field.layer.cornerRadius = 5.0
field.layer.borderWidth = 1.0
field.layer.borderColor = UIColor(red: 0.45, green: 0.46, blue: 0.50, alpha: 1.00).cgColor
// add it to the stack view
stackView.addArrangedSubview(field)
}
}
}
The result:
Your question didn't indicate how you want the UI to look on a wider device, so here's how that looks when the phone is rotated:

UIView width and height adjustment according to it subview programatically

I want to create something like this:
There's a white box under the buttons. If we are using SwiftUI logic, it's vertical padding : 5 and horizontal padding : 10, to create it with SwiftUI is pretty easy, but from what I have learned there is no padding and background color to a UIStackView and to create something like this, you need a UIView then add the stack view on top of the UIView.
This is what I have done so far:
//
// TransaksiViewController.swift
// HaselWiratama
//
// Created by Farhandika on 18/09/21.
// Copyright © 2021 Hasel.id. All rights reserved.
//
import UIKit
class TransaksiViewController: UIViewController {
let pesanButton: BigButton = {
let button = BigButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = .blue
button.configure(viewModel: MyCustomBigButton(title: "Phone",
imageName: "house", isSystemImage: false))
return button
}()
let ambulanceButton: BigButton = {
let button = BigButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = .blue
button.configure(viewModel: MyCustomBigButton(title: "Phone",
imageName: "house", isSystemImage: false))
return button
}()
let topStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.distribution = .fillEqually
stackView.spacing = 10
return stackView
}()
let uiView = UIView()
func configureUIView() {
//Configure the stackview
topStackView.addArrangedSubview(pesanButton)
topStackView.addArrangedSubview(ambulanceButton)
// add stack to UIView
uiView.addSubview(topStackView)
NSLayoutConstraint.activate([
topStackView.heightAnchor.constraint(equalToConstant: 150),
topStackView.centerYAnchor.constraint(equalTo: uiView.centerYAnchor),
topStackView.centerXAnchor.constraint(equalTo: uiView.centerXAnchor)
])
uiView.translatesAutoresizingMaskIntoConstraints = false
uiView.backgroundColor = .purple
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .cyan
view.addSubview(uiView)
configureUIView()
NSLayoutConstraint.activate([
uiView.widthAnchor.constraint(equalToConstant: 500),
uiView.heightAnchor.constraint(equalToConstant: 400),
uiView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
uiView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
}
/* Ignore the button width and height because I have not add the constraint yet */
The result:
As you can see, the width and height of the UIView is not relative to its child view.
How do I emulate the same padding horizontal 10 and vertical 5 in UIView?
(Basically a progressive or responsive width and height of a UIView.)
Since you have mentioned that you used constraints, see the following code. It reflect a UIViewController with what you need:
class ViewController: UIViewController {
// Horizontal Stackview
lazy var stack: UIStackView = {
let view = UIStackView()
view.translatesAutoresizingMaskIntoConstraints = false
view.axis = .horizontal
view.spacing = 10 // Inter-item space
view.backgroundColor = .white
view.distribution = .fillEqually // Setting distribution to fill equally
return view
}()
// Button 1
lazy var button1: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Button 1", for: .normal)
button.backgroundColor = .red
return button
}()
// Button 2
lazy var button2: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Button 2", for: .normal)
button.backgroundColor = .blue
return button
}()
// View that holds the stackview
lazy var stackHolder: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .white
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .gray
view.addSubview(stackHolder)
stackHolder.addSubview(stack)
stack.addArrangedSubview(button1)
stack.addArrangedSubview(button2)
//Setting layout constraints
NSLayoutConstraint.activate([
stackHolder.centerXAnchor.constraint(equalTo: view.centerXAnchor),
stackHolder.centerYAnchor.constraint(equalTo: view.centerYAnchor),
// Setting a width and height of the stack so that the `stackHolder` adjust relatively
stack.widthAnchor.constraint(equalToConstant: 250),
stack.heightAnchor.constraint(equalToConstant: 80),
// Setting the constraints with `constant` values for padding
stack.leadingAnchor.constraint(equalTo: stackHolder.leadingAnchor, constant: 5),
stack.trailingAnchor.constraint(equalTo: stackHolder.trailingAnchor, constant: -5),
stack.topAnchor.constraint(equalTo: stackHolder.topAnchor, constant: 10),
stack.bottomAnchor.constraint(equalTo: stackHolder.bottomAnchor, constant: -10),
])
}
}
This will output:

Embedd StackView in ScrollView that is embedded in a main StackView

Embedd StackView in ScrollView that is embedded in a main StackView
I am having trouble with a rather complicated detail view that I want to do programmatically. My view hierarchy looks something like this:
Since this might be better explained visualising, I have a screenshot here:
My problem is that I don't know how to set the height constraint on descriptionTextView – right now it's set to 400. What I want though is that it takes up all the space available as the middle item of the main stack view. Once one or more comments are added to the contentStackView, the text field should shrink.
I am not sure which constraints for which views I must set to achieve this...
Here's my take on it so far:
import UIKit
class DetailSampleViewController: UIViewController {
lazy var mainStackView: UIStackView = {
let m = UIStackView()
m.axis = .vertical
m.alignment = .fill
m.distribution = .fill
m.spacing = 10
m.translatesAutoresizingMaskIntoConstraints = false
m.addArrangedSubview(titleTextField)
m.addArrangedSubview(contentScrollView)
m.addArrangedSubview(footerStackView)
return m
}()
lazy var titleTextField: UITextField = {
let t = UITextField()
t.borderStyle = .roundedRect
t.placeholder = "Some Fancy Placeholder"
t.text = "Some Fancy Title"
t.translatesAutoresizingMaskIntoConstraints = false
return t
}()
lazy var contentScrollView: UIScrollView = {
let s = UIScrollView()
s.contentMode = .scaleToFill
s.keyboardDismissMode = .onDrag
s.translatesAutoresizingMaskIntoConstraints = false
s.addSubview(contentStackView)
return s
}()
lazy var contentStackView: UIStackView = {
let s = UIStackView()
s.translatesAutoresizingMaskIntoConstraints = false
s.axis = .vertical
s.alignment = .fill
s.distribution = .equalSpacing
s.spacing = 10
s.contentMode = .scaleToFill
s.addArrangedSubview(descriptionTextView)
s.addArrangedSubview(getCommentLabel(with: "Some fancy comment"))
s.addArrangedSubview(getCommentLabel(with: "Another fancy comment"))
s.addArrangedSubview(getCommentLabel(with: "And..."))
s.addArrangedSubview(getCommentLabel(with: "..even..."))
s.addArrangedSubview(getCommentLabel(with: "...more..."))
s.addArrangedSubview(getCommentLabel(with: "...comments..."))
s.addArrangedSubview(getCommentLabel(with: "Some fancy comment"))
s.addArrangedSubview(getCommentLabel(with: "Another fancy comment"))
s.addArrangedSubview(getCommentLabel(with: "And..."))
s.addArrangedSubview(getCommentLabel(with: "..even..."))
s.addArrangedSubview(getCommentLabel(with: "...more..."))
s.addArrangedSubview(getCommentLabel(with: "...comments..."))
return s
}()
lazy var descriptionTextView: UITextView = {
let tv = UITextView()
tv.font = UIFont.systemFont(ofSize: 17.0)
tv.clipsToBounds = true
tv.layer.cornerRadius = 5.0
tv.layer.borderWidth = 0.25
tv.translatesAutoresizingMaskIntoConstraints = false
tv.text = """
Some fancy textfield text,
spanning over multiple
lines
...
"""
return tv
}()
lazy var footerStackView: UIStackView = {
let f = UIStackView()
f.axis = .horizontal
f.alignment = .fill
f.distribution = .fillEqually
let commentLabel = UILabel()
commentLabel.text = "Comments"
let addCommentButton = UIButton(type: UIButton.ButtonType.system)
addCommentButton.setTitle("Add Comment", for: .normal)
f.addArrangedSubview(commentLabel)
f.addArrangedSubview(addCommentButton)
return f
}()
override func loadView() {
view = UIView()
view.backgroundColor = . systemBackground
navigationController?.isToolbarHidden = true
view.addSubview(mainStackView)
NSLayoutConstraint.activate([
mainStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 12),
mainStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -12),
mainStackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 12),
mainStackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -12),
titleTextField.heightAnchor.constraint(equalToConstant: titleTextField.intrinsicContentSize.height),
contentStackView.leadingAnchor.constraint(equalTo: contentScrollView.leadingAnchor),
contentStackView.trailingAnchor.constraint(equalTo: contentScrollView.trailingAnchor),
contentStackView.topAnchor.constraint(equalTo: contentScrollView.topAnchor),
contentStackView.bottomAnchor.constraint(equalTo: contentScrollView.bottomAnchor),
descriptionTextView.heightAnchor.constraint(equalToConstant: 400),
descriptionTextView.leadingAnchor.constraint(equalTo: mainStackView.leadingAnchor),
descriptionTextView.trailingAnchor.constraint(equalTo: mainStackView.trailingAnchor),
])
}
override func viewDidLoad() {
super.viewDidLoad()
title = "Detail View"
}
func getCommentLabel(with text: String) -> UILabel {
let l = UILabel()
l.layer.borderWidth = 0.25
l.translatesAutoresizingMaskIntoConstraints = false
l.text = text
return l
}
}
You're close, but a couple notes:
When using stack views - particularly inside scroll views - you sometimes need to explicitly define which elements can be stretched or not, and which elements can be compressed or not.
To get the scroll view filled before it has enough content, you need to set constraints so the combined content height is equal to the scroll view frame's height, but give that constraint a low priority so auto-layout can "break" it when you have enough vertical content.
A personal preference: I'm generally not a fan of adding subviews inside lazy var declarations. It can become confusing when trying to setup constraints.
I've re-worked your posted code to at least get close to what you're going for. It starts with NO comment labels... tapping the "Add Comment" button will add "numbered comment labels" and every third comment will wrap onto multiple lines.
Not really all that much in the way of changes... and I think I added enough comments to make things clear.
class DetailSampleViewController: UIViewController {
lazy var mainStackView: UIStackView = {
let m = UIStackView()
m.axis = .vertical
m.alignment = .fill
m.distribution = .fill
m.spacing = 10
m.translatesAutoresizingMaskIntoConstraints = false
// don't add subviews here
return m
}()
lazy var titleTextField: UITextField = {
let t = UITextField()
t.borderStyle = .roundedRect
t.placeholder = "Some Fancy Placeholder"
t.text = "Some Fancy Title"
t.translatesAutoresizingMaskIntoConstraints = false
return t
}()
lazy var contentScrollView: UIScrollView = {
let s = UIScrollView()
s.contentMode = .scaleToFill
s.keyboardDismissMode = .onDrag
s.translatesAutoresizingMaskIntoConstraints = false
// don't add subviews here
return s
}()
lazy var contentStackView: UIStackView = {
let s = UIStackView()
s.translatesAutoresizingMaskIntoConstraints = false
s.axis = .vertical
s.alignment = .fill
// distribution needs to be .fill (not .equalSpacing)
s.distribution = .fill
s.spacing = 10
s.contentMode = .scaleToFill
// don't add subviews here
return s
}()
lazy var descriptionTextView: UITextView = {
let tv = UITextView()
tv.font = UIFont.systemFont(ofSize: 17.0)
tv.clipsToBounds = true
tv.layer.cornerRadius = 5.0
tv.layer.borderWidth = 0.25
tv.translatesAutoresizingMaskIntoConstraints = false
tv.text = """
Some fancy textfield text,
spanning over multiple lines.
This textView now has a minimum height of 160-pts.
"""
return tv
}()
lazy var footerStackView: UIStackView = {
let f = UIStackView()
f.axis = .horizontal
f.alignment = .fill
f.distribution = .fillEqually
let commentLabel = UILabel()
commentLabel.text = "Comments"
let addCommentButton = UIButton(type: UIButton.ButtonType.system)
addCommentButton.setTitle("Add Comment", for: .normal)
// add a target so we can add comment labels
addCommentButton.addTarget(self, action: #selector(addCommentLabel(_:)), for: .touchUpInside)
// don't allow button height to be compressed
addCommentButton.setContentCompressionResistancePriority(.required, for: .vertical)
f.addArrangedSubview(commentLabel)
f.addArrangedSubview(addCommentButton)
return f
}()
// just for demo - numbers the added comment labels
var commentIndex: Int = 0
// do all this in viewDidLoad(), not in loadView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = . systemBackground
navigationController?.isToolbarHidden = true
title = "Detail View"
// add the mainStackView
view.addSubview(mainStackView)
// add elements to mainStackView
mainStackView.addArrangedSubview(titleTextField)
mainStackView.addArrangedSubview(contentScrollView)
mainStackView.addArrangedSubview(footerStackView)
// add contentStackView to contentScrollView
contentScrollView.addSubview(contentStackView)
// add descriptionTextView to contentStackView
contentStackView.addArrangedSubview(descriptionTextView)
// tell contentStackView to be the height of contentScrollView frame
let contentStackHeight = contentStackView.heightAnchor.constraint(equalTo: contentScrollView.frameLayoutGuide.heightAnchor)
// but give it a lower priority do it can grow as comment labels are added
contentStackHeight.priority = .defaultLow
NSLayoutConstraint.activate([
// constrain mainStackView top / bottom / leading / trailing to safe area
mainStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 12),
mainStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -12),
mainStackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 12),
mainStackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -12),
// title text field
titleTextField.heightAnchor.constraint(equalToConstant: titleTextField.intrinsicContentSize.height),
// minimum height for descriptionTextView
descriptionTextView.heightAnchor.constraint(greaterThanOrEqualToConstant: 160.0),
// constrain contentStackView top / leading / trailing / bottom to contentScrollView
contentStackView.topAnchor.constraint(equalTo: contentScrollView.topAnchor),
contentStackView.leadingAnchor.constraint(equalTo: contentScrollView.leadingAnchor),
contentStackView.trailingAnchor.constraint(equalTo: contentScrollView.trailingAnchor),
contentStackView.bottomAnchor.constraint(equalTo: contentScrollView.bottomAnchor),
// constrain contentStackView width to contentScrollView frame
contentStackView.widthAnchor.constraint(equalTo: contentScrollView.frameLayoutGuide.widthAnchor),
// activate contentStackHeight constraint
contentStackHeight,
])
// during dev, give some background colors so we can see the frames
contentScrollView.backgroundColor = .cyan
descriptionTextView.backgroundColor = .yellow
}
#objc func addCommentLabel(_ sender: Any?) -> Void {
// commentIndex is just used to number the added comments
commentIndex += 1
// let's make every third label end up with multiple lines, just to
// confirm variable-height labels won't mess things up
var s = "This is label \(commentIndex)"
if commentIndex % 3 == 0 {
s += ", and it has enough text that it should need to wrap onto multiple lines, even in landscape orientation."
}
let v = getCommentLabel(with: s)
// don't let comment labels stretch vertically
v.setContentHuggingPriority(.required, for: .vertical)
// don't let comment labels get compressed vertically
v.setContentCompressionResistancePriority(.required, for: .vertical)
contentStackView.addArrangedSubview(v)
// auto-scroll to bottom to show newly added comment label
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
let r = CGRect(x: 0.0, y: self.contentScrollView.contentSize.height - 1.0, width: 1.0, height: 1.0)
self.contentScrollView.scrollRectToVisible(r, animated: true)
}
}
func getCommentLabel(with text: String) -> UILabel {
let l = UILabel()
l.layer.borderWidth = 0.25
l.translatesAutoresizingMaskIntoConstraints = false
l.text = text
// allow wrapping / multi-line comments
l.numberOfLines = 0
return l
}
}

Set button width to fit dynamic button title

I have my UI structured say Level 1(UP), Level 2(DOWN) with some controls
In level 1, I have a label L1
In level 2, I have a button and label L2
In level 2 my button may be removed in runtime and I wanted my label L2 to be aligned to leading edge as L1
I'm facing two problems here
When I set my button title programmatically, I want to set my button such that its width grows when text increases and reduces its width when there is less text content. This isn't happening. Please see below screens the constraints I've in place
When I removed my button from superview, I wanted my L2 label Leading to be aligned to L1 leading. So I created a constraint from L2.leading = L1.leading and prioirty is 999
In this case, the button gets reduces its size to almost 0 even if i have text in that. Please advice me setting this up
Problem #1:
use .horizontal UIStackview for the button and text. set its distribution to .fill. For the button set contentCompression resistance priority to .required for .horizontal & set contenHugging priority to .required for .horizontal. So the Button will always wrap the text no matter what.
Problem #2:
While placing inside a stackview, you don't have to remove the button from superview. Just hide it using isHidden.
Code Demonstration
class SampleVC: UIViewController {
private var didAddConstraint = false
// Basic Views
private let label: UILabel = {
let view = UILabel()
view.translatesAutoresizingMaskIntoConstraints = false
view.text = "Label"
return view
}()
private let topButton: UIButton = {
let view = UIButton()
view.translatesAutoresizingMaskIntoConstraints = false
view.setTitle("Button", for: .normal)
view.setTitleColor(.gray, for: .highlighted)
view.backgroundColor = .green
view.setContentHuggingPriority(.required, for: .horizontal)
view.setContentCompressionResistancePriority(.required, for: .horizontal)
return view
}()
private let rightLabel: UILabel = {
let view = UILabel()
view.translatesAutoresizingMaskIntoConstraints = false
view.numberOfLines = 0
view.text = "label"
view.backgroundColor = .red
return view
}()
private lazy var stackview: UIStackView = {
let view = UIStackView()
view.translatesAutoresizingMaskIntoConstraints = false
view.axis = .horizontal
view.distribution = .fill
view.addArrangedSubview(topButton)
view.addArrangedSubview(rightLabel)
return view
}()
override func loadView() {
super.loadView()
view.addSubview(label)
view.addSubview(stackview)
view.setNeedsUpdateConstraints()
view.backgroundColor = .white
}
override func updateViewConstraints() {
super.updateViewConstraints()
if didAddConstraint == false {
didAddConstraint = true
// top label
label.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 16.0).isActive = true
label.topAnchor.constraint(equalTo: view.topAnchor, constant: 20).isActive = true
label.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
// stackview
stackview.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 16.0).isActive = true
stackview.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 8.0).isActive = true
stackview.rightAnchor.constraint(equalToSystemSpacingAfter: view.rightAnchor, multiplier: 16.0).isActive = true
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// TEST Code
// topButton.setTitle("TEST TEST TEST", for: .normal)
// topButton.isHidden = true
}
}

How to center button in horizonatal stackview?

I have a horizontal stackview with two buttons. Now I want, if one button is hidden other should be in the center.
like,
[x x]
[ x ]
Set the distribution attribute of the UIStackView to Fill Equally either from storyboard or programatically:
stackView.distribution = .fillEqually
Once a button is hidden, the other will be centered.
How about using some hidden UIViews?
Declare the stackView in your class:
var myFirstStack: UIStackView!
var bool = true
bool is a boolean that is going to simulate the condition when you'd want to hide or show the button.
Initialize the stackView in viewDidLoad:
myStack = UIStackView(arrangedSubviews: createButtons("1", "2"))
let v1 = UIView()
v1.isHidden = true
let v2 = UIView()
v2.isHidden = true
myStack.insertArrangedSubview(v1, at: 1)
myStack.addArrangedSubview(v2)
myStack.translatesAutoresizingMaskIntoConstraints = false
myStack.axis = .horizontal
myStack.spacing = 20
myStack.distribution = .fillEqually
view.addSubview(myStack)
It uses this function:
func createButtons(_ named: String...) -> [UIButton] {
var i = true
return named.map { name in
let btn = UIButton()
btn.translatesAutoresizingMaskIntoConstraints = false
btn.setTitle(name, for: .normal)
btn.backgroundColor = i ? .yellow : .red
i.toggle()
btn.setTitleColor(.black, for: .normal)
return btn
}
}
Add the autolayout constraints:
myFirstStack.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20).isActive = true
myFirstStack.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20).isActive = true
myFirstStack.heightAnchor.constraint(equalToConstant: 100).isActive = true
myFirstStack.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
Here is what it looks like so far:
When a third button is hit, the UIViews are hidden or shown:
#IBAction func hide(_ sender: Any) {
myFirstStack.arrangedSubviews[0].isHidden = bool //This is the button to hide
myFirstStack.spacing = bool ? -100 : 20 //Adjust the spacing to your liking
myFirstStack.arrangedSubviews[1].isHidden = !bool //Hide or show the first UIView
myFirstStack.arrangedSubviews[3].isHidden = !bool //Hide or show the second UIView
bool.toggle()
}
And here is the result:

Resources