Constrain Button with its own width - ios

Really struggling to constrain my buttons.
All I want is do setup my 2 buttons to have a height of 35 and their width should be whatever they need. Right now it looks like this (left & right buttons):
This is how I set them up:
let communityButton: UIButton = {
let v = UIButton()
v.setImage(UIImage(systemName: "person.3.fill"), for: .normal)
v.tintColor = UIColor.darkCustom
v.imageView?.contentMode = .scaleAspectFill
v.contentHorizontalAlignment = .fill
v.contentVerticalAlignment = .fill
v.translatesAutoresizingMaskIntoConstraints = false
v.addTarget(self, action: #selector(communityButtonTapped), for: .touchUpInside)
return v
}()
let profileButton: UIButton = {
let v = UIButton()
v.setImage(UIImage(systemName: "person.fill"), for: .normal)
v.tintColor = UIColor.darkCustom
v.imageView?.contentMode = .scaleAspectFill
v.contentHorizontalAlignment = .fill
v.contentVerticalAlignment = .fill
v.translatesAutoresizingMaskIntoConstraints = false
v.addTarget(self, action: #selector(profileButtonTapped), for: .touchUpInside)
return v
}()
Constraints:
//contrain communityButton
communityButton.centerYAnchor.constraint(equalTo: bottomBar.centerYAnchor),
communityButton.centerXAnchor.constraint(equalTo: bottomBar.centerXAnchor, constant: -view.frame.width/3.5),
communityButton.heightAnchor.constraint(equalToConstant: 35),
// constrain profileButton
profileButton.centerYAnchor.constraint(equalTo: bottomBar.centerYAnchor),
profileButton.centerXAnchor.constraint(equalTo: bottomBar.centerXAnchor, constant: view.frame.width/3.5),
profileButton.heightAnchor.constraint(equalToConstant: 35),
What is the right way to constrain here?

You might need to let autoLayout know that you want width to be flexible. You can do that by adding this line on both of your button definitions: -
// Replace "myButton" with your buttons name in your case "v"
myButton.autoresizingMask = [.flexibleWidth]
Edits: -
Since you are using system images, if you want your button to look bigger then you have to increase the font size on your image configuration. Edit your images by adding configuration with your preferred font sizes.
For example: -
let communityButton: UIButton = {
let v = UIButton()
//----
// NB: - Change the font size for bigger icons and viceversa
let imageSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: 50, weight: .regular, scale: .large)
v.setImage(UIImage(systemName: "person.3.fill", withConfiguration: imageSymbolConfiguration), for: .normal)
// -----
return v
}()
let profileButton: UIButton = {
let v = UIButton()
// ----
// NB: - Change the font size for bigger icons and viceversa
let imageSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: 50, weight: .regular, scale: .large)
v.setImage(UIImage(systemName: "person.fill", withConfiguration: imageSymbolConfiguration), for: .normal)
// ----
return v
}()
Explanation:-
From the official documentation
"Symbol image configuration objects include details such as the point size, scale, text style, weight, and font to apply to your symbol image. The system uses these details to determine which variant of the image to use and how to scale or style the image."

Related

Content Hugging Priority With UIImageView

I am laying out a similar UIStackView to Twitter's author layout as per below;
However, I am not getting the desired effect from setting the various priorities;
setContentHuggingPriority(UILayoutPriority(rawValue: 1), for: .horizontal)
The UIStackView contains the below items;
Display Name UILabel
Verified Badge UIImageView
Handle UILabel
Timestamp UILabel
The functionality I am looking for is the below;
The Timestamp and Verified Badge must not shrink at any time.
The Timestamp must take up the remaining space if the UIStackView doesn't fill the width
The Handle will shrink first
The Display Name will shrink last
How can I achieve this?
You can follow your "functionality I am looking for" list as you've written it...
The Timestamp and Verified Badge must not shrink at any time.
// don't let Timestamp compress
timeStampLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
// don't let "Dot" compress
dotLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
// badge image view is square (1:1 ratio)
// Width Anchor prevents both compression and expansion
badgeImageView.widthAnchor.constraint(equalTo: badgeImageView.heightAnchor).isActive = true
The Timestamp must take up the remaining space if the UIStackView doesn't fill the width
// don't let Display Name, Handle or Dot expand Horizontally
displayNameLabel.setContentHuggingPriority(.required, for: .horizontal)
handleLabel.setContentHuggingPriority(.required, for: .horizontal)
dotLabel.setContentHuggingPriority(.required, for: .horizontal)
The Handle will shrink first
// Handle shrink first
handleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
The Display Name will shrink last
// Display Name shrink next
displayNameLabel.setContentCompressionResistancePriority(.defaultLow + 1, for: .horizontal)
You didn't specify, so...
// Handle *could* to shrink to "no width"
// so use a min-Width to show at least a char or two
// if you want to allow it to disappear, comment out this line
handleLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 24.0).isActive = true
Here's a complete example -- it has two buttons... one to cycle through some sample data, and one to toggle background colors on the labels to see their frames. The stackView is added to the main view, but it will work the same when used in (what I assume is) your cell:
class TwitLineVC: UIViewController {
let displayNameLabel = UILabel()
let badgeImageView = UIImageView()
let handleLabel = UILabel()
let dotLabel = UILabel()
let timeStampLabel = UILabel()
let sampleData: [[String]] = [
["Stack Overflow", "#StackOverflow", "Nov 13"],
["Longer Display Name", "#LongerHandle", "Nov 14"],
["Much Longer Display Name", "#ThisHandleWillCompress", "Nov 15"],
["Much Longer Display Name Will Also Compress", "#ThisHandleWillCompress", "Nov 16"],
]
var idx: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
guard let img = UIImage(named: "twcheck") else {
fatalError("Could not load image!")
}
badgeImageView.image = img
let stackView = UIStackView()
stackView.spacing = 4
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
// don't let Display Name, Handle or Dot expand Horizontally
displayNameLabel.setContentHuggingPriority(.required, for: .horizontal)
handleLabel.setContentHuggingPriority(.required, for: .horizontal)
dotLabel.setContentHuggingPriority(.required, for: .horizontal)
// don't let Timestamp compress
timeStampLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
// don't let "Dot" compress
dotLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
// badge image view is square (1:1 ratio)
// Width Anchor prevents both compression and expansion
badgeImageView.widthAnchor.constraint(equalTo: badgeImageView.heightAnchor).isActive = true
// Handle shrink first
handleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
// Display Name shrink next
displayNameLabel.setContentCompressionResistancePriority(.defaultLow + 1, for: .horizontal)
// Handle *could* to shrink to "no width"
// so use a min-Width to show at least a char or two
// if you want to allow it to disappear, comment out this line
handleLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 24.0).isActive = true
// use Display Name label height to control stack view height
displayNameLabel.setContentHuggingPriority(.required, for: .vertical)
// add to stack view
stackView.addArrangedSubview(displayNameLabel)
stackView.addArrangedSubview(badgeImageView)
stackView.addArrangedSubview(handleLabel)
stackView.addArrangedSubview(dotLabel)
stackView.addArrangedSubview(timeStampLabel)
displayNameLabel.textColor = .white
handleLabel.textColor = .lightGray
dotLabel.textColor = .lightGray
timeStampLabel.textColor = .lightGray
let fSize: CGFloat = 12.0
displayNameLabel.font = .systemFont(ofSize: fSize, weight: .bold)
handleLabel.font = .systemFont(ofSize: fSize, weight: .regular)
dotLabel.font = handleLabel.font
timeStampLabel.font = handleLabel.font
// this never changes
dotLabel.text = "•"
// a button to cycle through sample data
let btn1 = UIButton(type: .system)
btn1.setTitle("Next Data Set", for: [])
btn1.addTarget(self, action: #selector(updateData(_:)), for: .touchUpInside)
btn1.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(btn1)
// a button to toggle background colors
let btn2 = UIButton(type: .system)
btn2.setTitle("Toggle Colors", for: [])
btn2.addTarget(self, action: #selector(toggleColors(_:)), for: .touchUpInside)
btn2.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(btn2)
NSLayoutConstraint.activate([
btn1.topAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 40.0),
btn1.centerXAnchor.constraint(equalTo: g.centerXAnchor),
btn2.topAnchor.constraint(equalTo: btn1.bottomAnchor, constant: 20.0),
btn2.centerXAnchor.constraint(equalTo: g.centerXAnchor),
])
// fill with the first data set
updateData(nil)
}
#objc func updateData(_ sender: Any?) {
displayNameLabel.text = sampleData[idx % sampleData.count][0]
handleLabel.text = sampleData[idx % sampleData.count][1]
timeStampLabel.text = sampleData[idx % sampleData.count][2]
idx += 1
}
#objc func toggleColors(_ sender: Any?) {
if displayNameLabel.backgroundColor == .clear {
displayNameLabel.backgroundColor = .systemGreen
handleLabel.backgroundColor = .systemBlue
dotLabel.backgroundColor = .systemYellow
timeStampLabel.backgroundColor = .systemRed
} else {
displayNameLabel.backgroundColor = .clear
handleLabel.backgroundColor = .clear
dotLabel.backgroundColor = .clear
timeStampLabel.backgroundColor = .clear
}
}
}

UIButton titleLabel has padding

UIButton title label has top and bottom padding, I want to remove padding.
Set UIButton content mode did not work.
Here is my code
lazy var button: UIButton = {
let button = UIButton(type: .custom)
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitleColor(ThemeColor.red, for: .normal)
button.setTitle("Push", for: .normal)
button.addTarget(self, action: #selector(buttonDidTap), for: .touchUpInside)
button.backgroundColor = .blue
button.contentHorizontalAlignment = .fill
button.contentVerticalAlignment = .fill
button.contentMode = .scaleAspectFill
return button
}()
and it looks like
How can I remove the padding space!
As Matt pointed out, you can fix this by adjusting the button's contentEdgeInsets
However, one thing I noticed, if you set the contentEdgeInsets to 0 all around:
button.contentEdgeInsets = UIEdgeInsets(top: 0,
left: 0,
bottom: 0,
right: 0)
You still get the the vertical padding for some reason.
I remember seeing an answer which I cannot find now where it suggested to set an extremely small edge inset and this should work:
lazy var button: UIButton = {
let button = UIButton(type: .custom)
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitleColor(.red, for: .normal)
button.setTitle("Push", for: .normal)
button.backgroundColor = .blue
button.contentHorizontalAlignment = .fill
button.contentVerticalAlignment = .fill
button.contentMode = .scaleAspectFill
button.contentEdgeInsets = UIEdgeInsets(top: .leastNormalMagnitude,
left: .leastNormalMagnitude,
bottom: .leastNormalMagnitude,
right: .leastNormalMagnitude)
return button
}()
You can use UIButton.Configuration and then set its contentInsets to .zero
lazy var button: UIButton = {
let button = UIButton(type: .custom)
button.translatesAutoresizingMaskIntoConstraints = false
var configuration = UIButton.Configuration.plain()
configuration.background.backgroundColor = .blue
configuration.background.cornerRadius = 0
configuration.baseForegroundColor = .red
configuration.title = "Push"
configuration.contentInsets = .zero
button.configuration = configuration
return button
}()
You can use the button's configuration to get more precise control over its appearance like so:
lazy var myButton: UIButton = {
let newButton = UIButton()
newButton.translatesAutoresizingMaskIntoConstraints = false
newButton.contentMode = .scaleAspectFill
newButton.addTarget(self, action: #selector(buttonDidTap), for: .touchUpInside)
// 'configurationUpdateHandler' property can be used to set appearance depending on its state
newButton.configurationUpdateHandler = { button in
switch button.state { // here i'll just use default so it's the same over all states
default:
button.configuration?.title = "Push"
button.configuration?.baseBackgroundColor = .blue
// remove padding here
button.configuration?.contentInsets.top = 0
button.configuration?.contentInsets.bottom = 0
}
}
return newButton
}()
Of course you don't need to use updateHandler, you can just access the configuration directly and just set it there
button.configuration?.contentInsets.top = 0
button.configuration?.contentInsets.bottom = 0
See if this solves the problem...

Swift - UIButton setting constraints programmatically

I am having problems with setting my height and width constraints for my UIButton. I have no idea why this code is not working:
let wishButton: UIButton = {
let v = UIButton()
v.setImage(UIImage(named: "wishButton"), for: .normal)
v.translatesAutoresizingMaskIntoConstraints = false
v.addTarget(self, action: #selector(wishButtonTapped), for: .touchUpInside)
return v
}()
These are my constraints:
wishButton.centerXAnchor.constraint(equalTo: popUpView.centerXAnchor).isActive = true
wishButton.centerYAnchor.constraint(equalTo: popUpView.centerYAnchor, constant: 150).isActive = true
wishButton.heightAnchor.constraint(equalToConstant: 100).isActive = true
wishButton.widthAnchor.constraint(equalToConstant: 100).isActive = true
This is how it looks at the moment:
It's weird because the other two constraints are working just fine. Probably just a stupid mistake but I am quite new and grateful for every help :)
Constraints have no issues. The issue is with the image. Your image is not big enough to fill the frame. You can set the .setBackgroundImage of the button to make it automatically scalable.
v.setBackgroundImage(UIImage(named: "wishButton"), for: .normal)
or you can make the content Alignment equal to .fill on both directions.
One thing I noticed is it doesn't look like you have set the frame of the button.
Try adding this line after you initialize the button:
v.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
I'm not sure if it is required but if your button isn't showing up at all, this may fix the issue
Add this to your UIButton initialisation, it will work
v.contentVerticalAlignment = .fill
v.contentHorizontalAlignment = .fill
It will scale your image.
let wishButton: UIButton = {
let v = UIButton()
v.setImage(UIImage(named: "wishButton"), for: .normal)
v.translatesAutoresizingMaskIntoConstraints = false
v.contentVerticalAlignment = .fill
v.contentHorizontalAlignment = .fill
v.addTarget(self, action: #selector(wishButtonTapped), for: .touchUpInside)
return v
}()

How to set setContentHuggingPriority programmatically

Im creating a view programmatically, but i can't set setContentHuggingPriority to work.
This is the current result:
private lazy var stackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
[button, text].forEach(stackView.addArrangedSubview)
return stackView
}()
private lazy var button: UIButton = {
let button = UIButton()
button.setImage(Asset.configIcon.image, for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
button.setContentHuggingPriority(.defaultHigh, for: .horizontal)
button.backgroundColor = .red
return button
}()
private lazy var text: UILabel = {
let label = UILabel()
label.text = "This line should have more width than the button"
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
label.setContentHuggingPriority(.defaultLow, for: .horizontal)
return label
}()
You are looking for the state that the button fits it's content and then the label takes any left space and fill it with multiline text.
Because you are using multiline label, You should set contentCompressionResistancePriority for both as well:
button.setContentCompressionResistancePriority(.required, for: .horizontal)
label.setContentCompressionResistancePriority(.required, for: .horizontal)

Swift 3 - Programmatically build a button with an image and label

I am currently trying to build an interface (programmatically) with 5 x buttons, containing an image and a label.
I have done this successfully for ONE button using a UIStackView (holding the UIButton and a UIlabel).
I have two questions for this forum…
A UIButton can be built to display a title OR an image, can it have both?
Can a ‘for in’ loop be used to generate 5 x individual buttons? i.e: a way to re-use code instead of typing out code for 5 x buttons, 5 x labels, 5 x stack views.
My working UIStackView button code is as follows:
// Button
let btnSettings = UIButton()
// btnSettings.setTitle("Settings", for: .normal)
btnSettings.setImage(#imageLiteral(resourceName: "star-in-circle"), for: .normal)
btnSettings.heightAnchor.constraint(equalToConstant: 100.0).isActive = true
btnSettings.widthAnchor.constraint(equalToConstant: 100.0).isActive = true
btnSettings.contentMode = .scaleAspectFit
btnSettings.addTarget(self, action: #selector(openSettings), for: .touchUpInside)
btnSettings.translatesAutoresizingMaskIntoConstraints = false
// Text Label
let textLabel = UILabel()
textLabel.backgroundColor = UIColor.clear
textLabel.widthAnchor.constraint(equalToConstant: 100.0).isActive = true
textLabel.heightAnchor.constraint(equalToConstant: 20.0).isActive = true
textLabel.font = UIFont.boldSystemFont(ofSize: 18)
textLabel.text = "Settings"
textLabel.textAlignment = .center
// Stack View
let stackView = UIStackView()
stackView.axis = UILayoutConstraintAxis.vertical
stackView.distribution = UIStackViewDistribution.equalSpacing
stackView.alignment = UIStackViewAlignment.center
stackView.spacing = 1.0
stackView.addArrangedSubview(btnSettings)
stackView.addArrangedSubview(textLabel)
stackView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(stackView)
// Constraints
stackView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
stackView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
Update / Solved Question 1
I needed to use the button.setBackgroundImage() in order for the button title to show up with the image.
btnSettings.setBackgroundImage(#imageLiteral(resourceName: "star-in-circle"), for: .normal)
btnSettings.setTitle("Button Title", for: .normal)
btnSettings.backgroundColor = UIColor.white
Of course, UIButton can have both an image or/and text. You can use:
button.setImage(image: UIImage?, for: UIControlState)
button.setTitle(title: String?, for: UIControlState)
Create a function that will return UIButton and do something like that:
let button = generateButton()
stackView.addArrangedSubview(button)

Resources