Swift - stop truncating UIButton titleLabel - swift

I have the following UIButton defined:
internal lazy var volumeUnitSelector: DropDownButton = {
let button = DropDownButton()
button.setTitle("impg", for: .normal)
button.setTitleColor(UIColor.darkGreyWithDarkMode.withAlphaComponent(0.7), for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 14)
button.titleLabel?.textAlignment = .right
if #available(iOS 13.0, *) {
let config = UIImage.SymbolConfiguration(pointSize: 8, weight: .black)
let symbol = UIImage(systemName: "arrowtriangle.down.fill", withConfiguration: config)?.withTintColor(UIColor.darkGreyWithDarkMode, renderingMode: .alwaysOriginal)
button.setImage(symbol, for: .normal)
}
return button
}()
And here is the DropDownButton class which subclasses UIButton:
class DropDownButton: UIButton {
override func layoutSubviews() {
self.titleEdgeInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 9)
super.layoutSubviews()
if imageView != nil {
imageView?.translatesAutoresizingMaskIntoConstraints = false
imageView?.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
imageView?.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
self.widthAnchor.constraint(equalToConstant: 43).isActive = true
}
}
}
Here's the challenge - I'm trying to set this button to have the text 'impg' and then an arrow to indicate a drop down menu to the right of the text. So as you can see, I've added the arrow as an SFSymbol and pinned it to the right of the button. I then need me text to be essentially pinned to the leadingAnchor of the arrow image, with a small margin (0.5 / 1 point).
So, I've set the titleEdgeInset to 9 on the right hand side to make room for the image. Now this is kind of working, but what I don't understand is that my title is being clipped, when there should be plenty of room for it.
Here is the current effect it has:
And here is the xCode layout view:
I don't understand why there's not enough space here to the left of the text to expand into. I have no other constraints set.

Related

Can we acces a UIStackView's subview's size?

My question is pretty simple but I haven't found any answer yet.
I am making a sort of table using two vertical stacks inside a horizontal stack. Both vStacks have different objects (Button in my case, with border for each one) but in the same quantity (so that they are horizontally paired like in a classic table).
I have set both of my vStack's distribution to .fillProportionally, and therefore each button have different size depending on their titleLabel length.
However, I would like to make each of my button have the same size of its paired button (the one next to him horizontally, in the other vStack) so that my cells borders would be aligned (using the biggest button's size as a reference in each pair).
I think it involves to find a way to access one stack's subview's size and then constraint the other stack subview to be equally sized. Or, because usually there is only one big button messing with the distribution and offsetting button pairs' border, accessing the way one stack displays its subviews and forcing the other stack to adopt the same way. Either way, I don't know how to do it yet.
I'd be glad if you could help me or lead me to the answer !
(I don't think I need to put code to explain my problem as it's a relatively abstract issue but if you need it I can share it)
EDIT :
Left : What I want, right : What I get
Each cell is a button (useless here but in my app it will have a functionality) with border , I want to set "description" button's height equal as "text" button. I hope it's clearer now :) I tried to invert the layout (two horizontal stacks in one vertical stack) but the issue is still here, with width instead of height this time.
EDIT 2 :
Following your advice, here is some code :
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var bottomButton: UIButton!
#IBOutlet weak var vstack: UIStackView!
#objc func buttonAction(sender: UIButton) {
sender.isEnabled = true
print(sender.frame)
}
override func viewDidLoad() {
super.viewDidLoad()
let button = newButton(text: "name")
let button2 = newButton(text: "John Smith")
let button3 = newButton(text: "Description")
let button4 = newButton(text: "text text text text text text text text text text text \n text text text text text \n text text text text text")
let hStack = UIStackView()
hStack.axis = .horizontal
hStack.distribution = .fillEqually
let hStack2 = UIStackView()
hStack2.axis = .horizontal
hStack2.distribution = .fillEqually
hStack.addArrangedSubview(button)
hStack.addArrangedSubview(button2)
hStack2.addArrangedSubview(button3)
hStack2.addArrangedSubview(button4)
vstack.addArrangedSubview(hStack)
vstack.addArrangedSubview(hStack2)
}
}
func newButton(text: String) -> UIButton {
let button = UIButton(type: .system)
button.isEnabled = true
button.setTitle(text, for: .disabled)
button.setTitle(text, for: .normal)
button.setTitleColor(.black, for: .disabled)
button.layer.borderWidth = 1
button.titleLabel?.numberOfLines = 0
button.titleLabel?.textAlignment = NSTextAlignment.center
return button
}
`
Using horizontal stacks in a vertical stack and Fill Equally partially solves the problem, because it only works when my text is under a certain length, otherwise it clips (see following image), which is why I was using fillProportionally.
OK - part of the problem is that you are modifying the titleLabel properties -- specifically, setting its .numberOfLines = 0. Auto-layout does not take that into account, without a little help.
You'll want to use a button subclass, such as this:
class MultiLineButton: UIButton {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
func commonInit() -> Void {
titleLabel?.numberOfLines = 0
titleLabel?.textAlignment = .center
// if you want to give your buttons some "padding" around the title
//contentEdgeInsets = UIEdgeInsets(top: 8.0, left: 8.0, bottom: 8.0, right: 8.0)
}
override var intrinsicContentSize: CGSize {
guard let tl = titleLabel else {
return .zero
}
let size = tl.intrinsicContentSize
return CGSize(width: size.width + contentEdgeInsets.left + contentEdgeInsets.right, height: size.height + contentEdgeInsets.top + contentEdgeInsets.bottom)
}
override func layoutSubviews() {
super.layoutSubviews()
guard let tl = titleLabel else { return }
tl.preferredMaxLayoutWidth = tl.frame.size.width
}
}
Using that class, here is an example view controller where we add a vertical stack view, and 2 horizontal "row" stack views:
class PruViewController: UIViewController {
func newButton(text: String) -> MultiLineButton {
let b = MultiLineButton()
b.titleLabel?.font = .systemFont(ofSize: 15.0)
b.setTitle(text, for: [])
b.setTitleColor(.blue, for: .normal)
b.setTitleColor(.lightGray, for: .highlighted)
b.setTitleColor(.black, for: .disabled)
b.layer.borderWidth = 1
b.layer.borderColor = UIColor.black.cgColor
return b
}
override func viewDidLoad() {
super.viewDidLoad()
let button = newButton(text: "name")
let button2 = newButton(text: "John Smith")
let button3 = newButton(text: "Description")
let button4 = newButton(text: "text text text text text text text text text text text \n text text text text text \n text text text text text")
let vStack = UIStackView()
vStack.axis = .vertical
vStack.distribution = .fill
let hStack = UIStackView()
hStack.axis = .horizontal
hStack.distribution = .fillEqually
let hStack2 = UIStackView()
hStack2.axis = .horizontal
hStack2.distribution = .fillEqually
hStack.addArrangedSubview(button)
hStack.addArrangedSubview(button2)
hStack2.addArrangedSubview(button3)
hStack2.addArrangedSubview(button4)
vStack.addArrangedSubview(hStack)
vStack.addArrangedSubview(hStack2)
vStack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(vStack)
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
vStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
vStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
vStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
}
}
The result:
And, you'll notice in my MultiLineButton class a comment about adding "padding" around the button title labels... here's how it looks with that line un-commented:

I am trying to create a UIbarButtonItem whose width and height is equal to 70% of the height of the navigationbar?

I am trying to write an extension fo UIBarButtonItem. I would like to use auto layout so that the UIBarButtonItem's width and height is 70% of the height of the UINavigationBar. When I implement this extension, i am getting the following auto layout errors:
import UIKit
extension UIBarButtonItem {
static func menuButton(target: Any?, action: Selector, imageName: String, navigationBar: UINavigationBar) -> UIBarButtonItem{
let button = UIButton.init(type: .system)
let image = UIImage.init(named: imageName)
button.setBackgroundImage(image, for: .normal)
button.addTarget(target, action: action, for: .touchUpInside)
let menuButton = UIBarButtonItem.init(customView: button)
menuButton.customView?.translatesAutoresizingMaskIntoConstraints = false
menuButton.customView?.widthAnchor.constraint(equalTo: navigationBar.heightAnchor, multiplier: 0.7).isActive = true
menuButton.customView?.heightAnchor.constraint(equalTo: navigationBar.heightAnchor, multiplier: 0.7).isActive = true
return menuButton
}
}
Implementation:
let rightButton = UIBarButtonItem.menuButton(target: self, action: #selector(editCells), imageName: "expand", navigationBar: navigationController!.navigationBar)
navigationItem.rightBarButtonItem = rightButton
Error message in console:
Terminating app due to uncaught exception 'NSGenericException', reason: 'Unable to activate constraint with anchors <NSLayoutDimension:0x600001be1180 "UIButton:0x7ff5d6d16b80.width"> and <NSLayoutDimension:0x600001be25c0 "UINavigationBar:0x7ff5d911a2f0.height"> because they have no common ancestor. Does the constraint or its anchors reference items in different view hierarchies? That's illegal.'
You can create custom view for your navigationBar custom button and set properties for whatever you want like this:
func addRightButton() {
let barButtonView = UIView(frame: CGRect(origin: .zero, size: CGSize(width: 180, height: 40)))
barButtonView.backgroundColor = UIColor.red
let navigationBarHeightWithRatio = (navigationController?.navigationBar.frame.height ?? 100) * 0.7
let customBarButton = UIButton(frame: CGRect(x: 0, y: 8, width: navigationBarHeightWithRatio, height: navigationBarHeightWithRatio))
customBarButton.setImage(UIImage(named: "your_image_name"), for: .normal)
customBarButton.setTitle("title", for: .normal)
customBarButton.addTarget(self, action: #selector("<your_action_function>"), for: .touchUpInside)
barButtonView.addSubview(customBarButton)
let rightBarButton = UIBarButtonItem(customView: barButtonView)
navigationItem.rightBarButtonItem = rightBarButton
}
Note:
You can customize width different width, height values and x, y positions.
I hope it is works.
Enjoy.
The error message is clear. When you say:
let rightButton = UIBarButtonItem.menuButton(
target: self, action: #selector(editCells),
imageName: "expand",
navigationBar: navigationController!.navigationBar)
... and the menuButton method runs, at that moment, you are trying to form a constraint relationship between the button and the navigation bar at a time when the button is not in the navigation bar. That, as the error message tells you, is illegal.
(I doubt that the goal you have outlined is possible at all, but that's another story. I'm just explaining the error message.)

Create UIButtons with dynamic font size but all share same font size in UIStackView

I am using UIStackView and adding three buttons to it. I want it so that the button with the most text (B1) will be auto resized to fit the width and the other buttons will share the same font size as B1.
#IBOutlet weak var stackView: UIStackView!
var btnTitles = [String]()
btnTitles.append("Practice Exams")
btnTitles.append("Test Taking Tips")
btnTitles.append("About")
createButtons(buttonTitles: btnTitles)
var min = CGFloat(Int.max) // keep track of min font
func createButtons(buttonTitles: [String]) {
var Buttons = [UIButton]()
for title in buttonTitles {
let button = makeButtonWithText(text: title)
// set the font to dynamically size
button.titleLabel!.numberOfLines = 1
button.titleLabel!.adjustsFontSizeToFitWidth = true
button.titleLabel!.baselineAdjustment = .alignCenters // I think it keeps it centered vertically
button.contentEdgeInsets = UIEdgeInsetsMake(5, 10, 5, 10); // set margins
if (button.titleLabel?.font.pointSize)! < min {
min = (button.titleLabel?.font.pointSize)! // to get the minimum font size of any of the buttons
}
stackView.addArrangedSubview(button)
Buttons.append(button)
}
}
func makeButtonWithText(text:String) -> UIButton {
var myButton = UIButton(type: UIButtonType.system)
//Set a frame for the button. Ignored in AutoLayout/ Stack Views
myButton.frame = CGRect(x: 30, y: 30, width: 150, height: 100)
// background color - light blue
myButton.backgroundColor = UIColor(red: 0.255, green: 0.561, blue: 0.847, alpha: 1)
//State dependent properties title and title color
myButton.setTitle(text, for: UIControlState.normal)
myButton.setTitleColor(UIColor.white, for: UIControlState.normal)
// set the font to dynamically size
myButton.titleLabel!.font = myButton.titleLabel!.font.withSize(70)
myButton.contentHorizontalAlignment = .center // align center
return myButton
}
I wanted to find the minimum font size and then set all the buttons to the minimum in viewDidAppear button the font prints as 70 for all of them even though they clearly appear different sizes (see image)
override func viewDidAppear(_ animated: Bool) {
print("viewDidAppear")
let button = stackView.arrangedSubviews[0] as! UIButton
print(button.titleLabel?.font.pointSize)
let button1 = stackView.arrangedSubviews[1] as! UIButton
print(button1.titleLabel?.font.pointSize)
let button2 = stackView.arrangedSubviews[2] as! UIButton
print(button2.titleLabel?.font.pointSize)
}
image
You can try playing around with this func to return the scaled-font-size of a label:
func actualFontSize(for aLabel: UILabel) -> CGFloat {
// label must have text, must have .minimumScaleFactor and must have .adjustsFontSizeToFitWidth == true
guard let str = aLabel.text,
aLabel.minimumScaleFactor > 0.0,
aLabel.adjustsFontSizeToFitWidth
else { return aLabel.font.pointSize }
let attributes = [NSAttributedString.Key.font : aLabel.font]
let attStr = NSMutableAttributedString(string:str, attributes:attributes as [NSAttributedString.Key : Any])
let context = NSStringDrawingContext()
context.minimumScaleFactor = aLabel.minimumScaleFactor
_ = attStr.boundingRect(with: aLabel.bounds.size, options: .usesLineFragmentOrigin, context: context)
return aLabel.font.pointSize * context.actualScaleFactor
}
On viewDidAppear() you would loop through the buttons, getting the smallest actual font size, then set the font size for each button to that value.
It will take some experimentation... For one thing, I've noticed in the past that font-sizes can get rounded - so setting a label's font point size to 20.123456789 won't necessarily give you that exact point size. Also, since this changes the actual font size assigned to the labels, you'll need to do some resetting if you change the button title dynamically. Probably also need to account for button frame changes (such as with device rotation, etc).
But... here is a quick test that you can run to see the approach:
class TestViewController: UIViewController {
let stackView: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .vertical
v.alignment = .center
v.distribution = .fillEqually
v.spacing = 8
return v
}()
var btnTitles = [String]()
var theButtons = [UIButton]()
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
fixButtonFonts()
}
func setupUI() -> Void {
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 40),
stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -40),
])
btnTitles.append("Practice Exams")
btnTitles.append("Test Taking Tips")
btnTitles.append("About")
createButtons(buttonTitles: btnTitles)
}
func fixButtonFonts() -> Void {
var minActual = CGFloat(70)
// get the smallest actual font size
theButtons.forEach { btn in
if let lbl = btn.titleLabel {
let act = actualFontSize(for: lbl)
// for debugging
//print("actual font size: \(act)")
minActual = Swift.min(minActual, act)
}
}
// set font size for each button
theButtons.forEach { btn in
if let lbl = btn.titleLabel {
lbl.font = lbl.font.withSize(minActual)
}
}
}
func createButtons(buttonTitles: [String]) {
for title in buttonTitles {
let button = makeButtonWithText(text: title)
// set the font to dynamically size
button.titleLabel!.numberOfLines = 1
button.titleLabel!.adjustsFontSizeToFitWidth = true
// .minimumScaleFactor is required
button.titleLabel!.minimumScaleFactor = 0.05
button.titleLabel!.baselineAdjustment = .alignCenters // I think it keeps it centered vertically
button.contentEdgeInsets = UIEdgeInsets(top: 5, left: 10, bottom: 5, right: 10); // set margins
stackView.addArrangedSubview(button)
theButtons.append(button)
}
}
func makeButtonWithText(text:String) -> UIButton {
let myButton = UIButton(type: UIButton.ButtonType.system)
//Set a frame for the button. Ignored in AutoLayout/ Stack Views
myButton.frame = CGRect(x: 30, y: 30, width: 150, height: 100)
// background color - light blue
myButton.backgroundColor = UIColor(red: 0.255, green: 0.561, blue: 0.847, alpha: 1)
//State dependent properties title and title color
myButton.setTitle(text, for: UIControl.State.normal)
myButton.setTitleColor(UIColor.white, for: UIControl.State.normal)
// set the font to dynamically size
myButton.titleLabel!.font = myButton.titleLabel!.font.withSize(70)
myButton.contentHorizontalAlignment = .center // align center
return myButton
}
func actualFontSize(for aLabel: UILabel) -> CGFloat {
// label must have text, must have .minimumScaleFactor and must have .adjustsFontSizeToFitWidth == true
guard let str = aLabel.text,
aLabel.minimumScaleFactor > 0.0,
aLabel.adjustsFontSizeToFitWidth
else { return aLabel.font.pointSize }
let attributes = [NSAttributedString.Key.font : aLabel.font]
let attStr = NSMutableAttributedString(string:str, attributes:attributes as [NSAttributedString.Key : Any])
let context = NSStringDrawingContext()
context.minimumScaleFactor = aLabel.minimumScaleFactor
_ = attStr.boundingRect(with: aLabel.bounds.size, options: .usesLineFragmentOrigin, context: context)
return aLabel.font.pointSize * context.actualScaleFactor
}
}
Result:

resolving the issue coming with text field and button image issue

I am new in swift.I have created simple login screen .I have two issues coming with loginviewController.swift
Issue 1 # i dont know how to compare the two image that is displayed in button . I have used if checkbox.setImage(img, for: .normal) for comparing image that is displayed in side button for toggle action between checked and unchecked
let img = UIImage(named:"check box#1x.png")
let img2 = UIImage(named:"uncheck box.png")
#IBOutlet weak var checkbox: UIButton!
#IBAction func checkbox(_ sender: Any) {
if checkbox.setImage(img, for: .normal)
{
checkbox.setImage(img , for: .normal)
}
else{
checkbox.setImage(img2, for: .normal)
}
}
Issue 2 # I am trying to high light the bottom borders of text field .i am writing the code for highlighting two text field but it is highlighting only single text field
func boaderSetting() {
let border = CALayer()
let width = CGFloat(1.0)
border.borderColor = UIColor.orange.cgColor
border.frame = CGRect(x: 0, y: username_input.frame.size.height - width, width: username_input.frame.size.width, height: username_input.frame.size.height)
border.borderWidth = width
username_input.layer.addSublayer(border)
username_input.layer.masksToBounds = true
///
let border1 = CALayer()
let width1 = CGFloat(1.0)
border1.borderColor = UIColor.orange.cgColor
border1.frame = CGRect(x: 0, y: password_input.frame.size.height - width1, width: password_input.frame.size.width, height: password_input.frame.size.height)
border1.borderWidth = width1
password_input.layer.addSublayer(border)
password_input.layer.masksToBounds = true
}
how to
->compare image displayed in button with other image
-> hight light the bottom border of both text field .
you can download the project from this link https://drive.google.com/file/d/1zjNUBXZ-9WL4DTglhMXIlN-TpaO5IXBz/view?usp=sharing
To check against an image in the button you can use button.currentImage == image. In your example it will look like this
#IBAction func checkbox(_ sender: Any) {
if checkbox.currentImage == img2 {
checkbox.setImage(img , for: .normal)
} else {
checkbox.setImage(img2, for: .normal)
}
}
It is only showing a highlight on the bottom text field border as there is a mistake in your example here password_input.layer.addSublayer(border), you want to use border1 here instead.
func boaderSetting() {
let border = CALayer()
let width = CGFloat(1.0)
border.borderColor = UIColor.orange.cgColor
border.frame = CGRect(x: 0, y: username_input.frame.size.height - width, width: username_input.frame.size.width, height: username_input.frame.size.height)
border.borderWidth = width
username_input.layer.addSublayer(border)
username_input.layer.masksToBounds = true
///
let border1 = CALayer()
let width1 = CGFloat(1.0)
border1.borderColor = UIColor.orange.cgColor
border1.frame = CGRect(x: 0, y: password_input.frame.size.height - width1, width: password_input.frame.size.width, height: password_input.frame.size.height)
border1.borderWidth = width1
password_input.layer.addSublayer(border1)
password_input.layer.masksToBounds = true
}
For #1 you can simply set images for different states.
button.setImage(UIImage(named: "imageForSelected"), for : .selected)
button.setImage(UIImage(named: "imageForNotSelected"), for : .normal)
And then somewhere in your logic set button state
button.setSelected(true)

How to add button layout constraints ARSKView?

I'm using SpriteKit (ARSKView) with UIKit elements, and I'd like to add a button that stays in the bottom left of the screen at all times. When pressed, the button would clear all nodes.
This is the code I tried to get the button to display, but when I try adding the bottom anchor and left anchor constraints, I get this error
Unable to activate constraint with anchors
and because they have no common ancestor.
Does the constraint or its anchors reference items in different view
hierarchies? That's illegal.'
The button works fine when tapped, but how can I add layout constraints in ARSKView?
And how do I get the button to clear all the nodes from the scene?
Here is my code
override func viewDidLoad() {
super.viewDidLoad()
// Set the view's delegate
sceneView.delegate = self
// Show statistics such as fps and node count
sceneView.showsFPS = true
sceneView.showsNodeCount = true
// sceneView.showsPhysics = true
// Load the SKScene from 'Scene.sks'
if let scene = SKScene(fileNamed: "Scene") {
sceneView.presentScene(scene)
}
let button = UIButton(frame: CGRect(x: 100, y: 100, width: 100, height: 50))
button.backgroundColor = UIColor(red: 0, green: 0, blue:0, alpha: 0.4)
button.setTitle("Clear Screen", for: .normal)
button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
button.bottomAnchor.constraint(equalTo: sceneView.bottomAnchor).isActive = true
button.leftAnchor.constraint(equalTo: sceneView.leftAnchor).isActive = true
self.view.addSubview(button)
}
#objc func buttonAction(sender: UIButton!) {
print("Button tapped")
}