Label top alignment - ios

I'm trying to put the temperature in my app. I would like to show it like that:
But all I'm able to get is that:
I have tried to use this code to align the two label on top:
#IBDesignable class TopAlignedLabel: UILabel {
override func drawText(in rect: CGRect) {
if let stringText = text {
let stringTextAsNSString = stringText as NSString
let labelStringSize = stringTextAsNSString.boundingRect(with: CGSize(width: self.frame.width,height: CGFloat.greatestFiniteMagnitude), options: NSStringDrawingOptions.usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil).size
super.drawText(in: CGRect(x:0,y: 0,width: self.frame.width, height:ceil(labelStringSize.height)))
} else {
super.drawText(in: rect)
}
}
}
It worked on for the '°C' but it's not working for the 25. Can someone help me find a solution to my problem ?

A very simple way to solve this is with attributed strings:
let tempText = "25˚C"
let baseFont = UIFont.systemFont(ofSize: 23.0)!
let superscriptFont = UIFont.systemFont(ofSize: 15.0)!
let attrStr = NSMutableAttributedString(string: tempText, attributes: [NSFontAttributeName: baseFont])
attrStr.addAttributes([NSFontAttributeName: superscriptFont, NSBaselineOffsetAttributeName: 10.0], range: NSMakeRange(2,2))
myLabel.attributedText = attrStr
You can keep adding more different attributes on any range you want by using the addAttributes method.

A font has multiple characteristics, including Ascender, Descender, CapHeight, etc... So what your code gets is not a way to align the character glyphs flush with the top of the label frame, but rather it aligns the Font bounding box to the top of the frame.
Calculating the offset between font metrics might give you what you're after. Here is one example (you can run it in a Playground page):
import UIKit
import PlaygroundSupport
class TestViewController : UIViewController {
let labelOne: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 60.0)
label.text = "25"
label.backgroundColor = .cyan
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 1
label.textAlignment = .right
return label
}()
let labelTwo: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 24.0)
label.text = "°C"
label.backgroundColor = .cyan
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 1
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
// add the scroll view to self.view
self.view.addSubview(labelOne)
self.view.addSubview(labelTwo)
// constrain the scroll view to 8-pts on each side
labelOne.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 8.0).isActive = true
labelOne.topAnchor.constraint(equalTo: view.topAnchor, constant: 8.0).isActive = true
labelTwo.leftAnchor.constraint(equalTo: labelOne.rightAnchor, constant: 2.0).isActive = true
if let f1 = labelOne.font, let f2 = labelTwo.font {
let offset = (f1.ascender - f1.capHeight) - (f2.ascender - f2.capHeight)
labelTwo.topAnchor.constraint(equalTo: labelOne.topAnchor, constant: offset).isActive = true
}
}
}
let vc = TestViewController()
vc.view.backgroundColor = .yellow
PlaygroundPage.current.liveView = vc
And this is the result:
Depending on what font you are actually using, though, that may not be good enough. If so, you'll want to look into CoreText / CTFont / CTFontGetGlyphsForCharacters() / etc.

Related

Center text vertically in line of NSMutableAttributedString

I'm using NSMutableAttributedString to setup text style. When I set .minimumLineHeight, it centre text in line to the bottom of line. I would like to customise somehow this alignment to centre text in line vertically.
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.minimumLineHeight = fontLineHeight // 24 for example
attributedText.addAttribute(.paragraphStyle, value: paragraphStyle, range: fullRange)
I want to get such result:
The problem with providing a lineHeight is that you override the default height-calculation-behaviour with a hardcoded value. Even if you've provided the font's line height as the value, it can vary dynamically based on provided content and it's best to leave this to auto layout (Refer https://stackoverflow.com/a/33278748/9293498)
Auto-layout has a solution for these issues most of the time. All you need is a proper constraint setup for your label (and lineSpacing in your scenario) which can enable it to scale automatically based on provided text with just-enough space required. Both NSAttributedString and String values should work
Here's a code sample I've written trying to simulate your requirement:
Views:
private let termsAndConditionsContainer: UIStackView = {
let container = UIStackView()
container.backgroundColor = .clear
container.spacing = 16
container.axis = .vertical
container.alignment = .leading
container.distribution = .fill
container.translatesAutoresizingMaskIntoConstraints = false
return container
}()
private let dataAgreementButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(UIImage(), for: .normal)
button.layer.borderColor = UIColor.gray.cgColor
button.layer.borderWidth = 0.5
button.layer.cornerRadius = 16
return button
}()
private let dataAgreementLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 17, weight: .medium)
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let tosAgreementButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(UIImage(), for: .normal)
button.layer.borderColor = UIColor.gray.cgColor
button.layer.borderWidth = 0.5
button.layer.cornerRadius = 16
return button
}()
private let tosAgreementLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 17, weight: .medium)
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
Functions:
private func attributedString(with text: String) -> NSAttributedString {
//Leave it to auto-layout to determine the line height
let attributedString = NSMutableAttributedString(string: text)
let style = NSMutableParagraphStyle()
style.lineSpacing = 8 // I've just declared the spacing between lines
attributedString.addAttribute(.paragraphStyle, value: style, range: NSRange(location: 0, length: attributedString.length))
return attributedString
}
private func generateRowContainer() -> UIStackView {
let container = UIStackView()
container.backgroundColor = .clear
container.spacing = 16
container.axis = .horizontal
container.alignment = .center
container.distribution = .fill
container.layer.borderWidth = 0.5
container.layer.borderColor = UIColor.green.cgColor
container.translatesAutoresizingMaskIntoConstraints = false
return container
}
And here's my constraint setup as I add the above views in viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
let dataAgreementContainer = generateRowContainer()
dataAgreementContainer.addArrangedSubview(dataAgreementButton)
dataAgreementLabel.attributedText = attributedString(with: "I agree with the Information note regarding personal data processing")
dataAgreementContainer.addArrangedSubview(dataAgreementLabel)
termsAndConditionsContainer.addArrangedSubview(dataAgreementContainer)
let tosAgreementContainer = generateRowContainer()
tosAgreementContainer.addArrangedSubview(tosAgreementButton)
tosAgreementLabel.attributedText = attributedString(with: "I agree with terms and conditions")
tosAgreementContainer.addArrangedSubview(tosAgreementLabel)
termsAndConditionsContainer.addArrangedSubview(tosAgreementContainer)
view.addSubview(termsAndConditionsContainer)
NSLayoutConstraint.activate([
dataAgreementButton.widthAnchor.constraint(equalToConstant: 32),
dataAgreementButton.heightAnchor.constraint(equalToConstant: 32),
tosAgreementButton.widthAnchor.constraint(equalToConstant: 32),
tosAgreementButton.heightAnchor.constraint(equalToConstant: 32),
termsAndConditionsContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 32),
termsAndConditionsContainer.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -32),
termsAndConditionsContainer.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
I got the below output:
In the above example, I've used a horizontal stack view to maintain the alignment of text and radio button, which seems efficient to me for your requirement. But the point is, as long as your label is in a properly constrained environment, you don't have the need to manually specify any form of its height.

Specifying NSLineBreakMode.byTruncatingTail causes issues when displaying NSAttributedString in UILabel

In my app, I fetch some HTML strings from the Wordpress REST API of a website. I use the following extension to convert the HTML to an NSAttributedString that I can display in my UILabel:
extension NSAttributedString {
convenience init(htmlString html: String, font: UIFont? = nil, useDocumentFontSize: Bool = false) throws {
let options: [NSAttributedString.DocumentReadingOptionKey : Any] = [
.documentType: NSAttributedString.DocumentType.html,
.characterEncoding: String.Encoding.utf8.rawValue
]
let data = html.data(using: .utf8, allowLossyConversion: true)
guard (data != nil), let fontFamily = font?.familyName, let attr = try? NSMutableAttributedString(data: data!, options: options, documentAttributes: nil) else {
try self.init(data: data ?? Data(html.utf8), options: options, documentAttributes: nil)
return
}
let fontSize: CGFloat? = useDocumentFontSize ? nil : font!.pointSize
let range = NSRange(location: 0, length: attr.length)
attr.enumerateAttribute(.font, in: range, options: .longestEffectiveRangeNotRequired) { attrib, range, _ in
if let htmlFont = attrib as? UIFont {
let traits = htmlFont.fontDescriptor.symbolicTraits
var descrip = htmlFont.fontDescriptor.withFamily(fontFamily)
if (traits.rawValue & UIFontDescriptor.SymbolicTraits.traitBold.rawValue) != 0 {
descrip = descrip.withSymbolicTraits(.traitBold)!
}
if (traits.rawValue & UIFontDescriptor.SymbolicTraits.traitItalic.rawValue) != 0 {
descrip = descrip.withSymbolicTraits(.traitItalic)!
}
attr.addAttribute(.font, value: UIFont(descriptor: descrip, size: fontSize ?? htmlFont.pointSize), range: range)
attr.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.label, range: range)
}
}
self.init(attributedString: attr)
}
}
The UILabel is defined as follows:
let myLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.systemFont(ofSize: 15, weight: .regular)
return label
}()
I use the following code to display the NSAttributedString in the UILabel:
let contentAttributed = try? NSAttributedString(htmlString: html, font: UIFont.systemFont(ofSize: 15, weight: .regular))
myLabel.attributedText = contentAttributed
myLabel.numberOfLines = 3
With this, everything works as expected, as shown in two examples below:
Example 1 (everything ok)
Example 2 (everything ok)
I would really like the UILabels to be completely filled and with ellipsis at the end (truncated lines). If I add myLabel.lineBreakMode = .byTruncatingTail into my code, there are weird issues, as shown in the same examples below. The weird thing is that some of the UILabels still work absolutely fine (example 2), while others show the weird issues (example 1).
Example 1 (issues)
Example 2 (still everything ok)
If anyone knows where in this rather complicated issue I should start digging, any help would be greatly appreciated.
Edit: Some examples of HTML strings that produce the issue:
\n<p>Liebe Damen & Herren</p>\n\n\n\n<p>Erneut trifft es uns und wir müssen die Sammlung vom Samstag <strong>bis auf Weiteres verschieben!</strong>
\n<p>\n\nLiebe Kollegen</p>\n\n\n\n<p>Am 28. August führen wir (endlich) wieder die Papiersammlung in der Gemeinde durch. Dabei sind wir auf möglichst viele Helfer:innen angewiesen.</p>
\n<p>​Liebe Kollegen</p>\n\n\n\n<p>Nur ungerne stören wir eure Sommerferien mit einer unerfreulichen Nachricht: <strong>Das Katre 2021 vom 4. und 5. September wurde leider abgesagt!</strong></p>
The following HTML strings do not produce the issue:
\n<p>Kürzlich hat der Bundesrat über die in Etappen stattfindende Lockerung der Massnahmen zum Schutz vor dem Coronavirus informiert. Leider haben die ersten Lockerungen des Bundes noch keinen Einfluss auf uns.</p>
<p>Liebe Eltern und Kollegen</p>\n<p>Unser Jahresprogramm für 2019 ist nun fertig und online. Ihr findet es unter Medien -> Downloads oder direkt hier:</p>\n<p>Jahresprogramm_2019</p>
The App is run on the iPhone 13 Pro Simulator in portrait orientation. The UILabel is part of a custom UITableViewCell. The UITableView is aligned to the safe area on the left and right (constant 0). The UILabel is aligned to the contentView.leadingAnchor and contentView.trailingAnchor with a constant of 30 and -30, respectively.
I'm posting this answer, not because it is a solution but because I think it is worth discussion...
Edit
Personally, I would say Attributed Text and .lineBreakMode = .byTruncatingTail is rather buggy.
This does not appear to be related to Attributed Text, but rather to the presence of a newline character.
For example, if the label is set to .numberOfLines = 3 and we use this text:
myLabel.text = "Let's test this:\nPlenty of text to wrap onto more than three lines. Note this is plain text, not attributed text. It may or may not exhibit the same problems."
we will see the same corrupted text wrapping occur.
Also note: this does NOT happen on iOS prior to 15.4!
/Edit
In all the following examples, the same attributed string is used for pairs of rows, but only the cyan labels have myLabel.lineBreakMode = .byTruncatingTail.
First note:
If we set myLabel.numberOfLines = 0, we get this output:
Which doesn't make much sense... Apparently, a newline character gets factored in at the end of the attributed string -- but even so, we get almost, but not enough space for TWO additional lines (I'm guessing it's a newline and a paragraph vertical space?):
Second note:
If we DO NOT have enough text to exceed the bounds of the label (again, we're setting myLabel.numberOfLines = 3 here), we get another oddity. This is when I rotate the device:
Even though the text is NOT truncated, we get the ... -- because UIKit is trying to add 1.5 additional blank lines at the end of the string.
Third note:
As I mentioned in a comment, I know we came across this once before here on SO -- unfortunately, I can't remember if we resolved it or (probably) not.
To reproduce with Plain Text
class WrapBugVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let stackView = UIStackView()
stackView.axis = .vertical
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.widthAnchor.constraint(equalToConstant: 315.0),
stackView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
])
var vInfo: UILabel
let testStr1: String = "Let's test this (no newline character): Plenty of text to wrap onto more than three lines. Note this is plain text, not attributed text."
let testStr2: String = "Let's test this:\nPlenty of text to wrap onto more than three lines. Note this is plain text, not attributed text."
vInfo = UILabel()
vInfo.font = .italicSystemFont(ofSize: 15.0)
vInfo.text = "NO newline, .byWordWrapping"
stackView.addArrangedSubview(vInfo)
let v1 = UILabel()
v1.numberOfLines = 3
v1.text = testStr1
v1.backgroundColor = .yellow
v1.lineBreakMode = .byWordWrapping
stackView.addArrangedSubview(v1)
vInfo = UILabel()
vInfo.font = .italicSystemFont(ofSize: 15.0)
vInfo.text = "NO newline, .byTruncatingTail"
stackView.addArrangedSubview(vInfo)
let v2 = UILabel()
v2.numberOfLines = 3
v2.text = testStr1
v2.backgroundColor = .cyan
v2.lineBreakMode = .byTruncatingTail
stackView.addArrangedSubview(v2)
vInfo = UILabel()
vInfo.font = .italicSystemFont(ofSize: 15.0)
vInfo.text = "WITH newline, .byWordWrapping"
stackView.addArrangedSubview(vInfo)
let v3 = UILabel()
v3.numberOfLines = 3
v3.text = testStr2
v3.backgroundColor = .yellow
v3.lineBreakMode = .byWordWrapping
stackView.addArrangedSubview(v3)
vInfo = UILabel()
vInfo.font = .italicSystemFont(ofSize: 15.0)
vInfo.text = "WITH newline, .byTruncatingTail"
stackView.addArrangedSubview(vInfo)
let v4 = UILabel()
v4.numberOfLines = 3
v4.text = testStr2
v4.backgroundColor = .cyan
v4.lineBreakMode = .byTruncatingTail
stackView.addArrangedSubview(v4)
[v1, v2, v3, v4].forEach { v in
stackView.setCustomSpacing(16.0, after: v)
}
}
}
Result:
Workaround
You may want to try this as a workaround...
class TextViewLabel: UITextView {
public var numberOfLines: Int = 0 {
didSet {
textContainer.maximumNumberOfLines = numberOfLines
}
}
public var lineBreakMode: NSLineBreakMode = .byTruncatingTail {
didSet {
textContainer.lineBreakMode = lineBreakMode
}
}
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() -> Void {
isScrollEnabled = false
isEditable = false
isSelectable = false
textContainerInset = UIEdgeInsets.zero
textContainer.lineFragmentPadding = 0
}
}
Using TextViewLabel in place of UILabel will avoid the bug:
Edit 2
As of iOS 16.2 there are still inconsistencies with word-wrapping and tail-truncation between UILabel and UITextView.
Here is some code to see them side-by-side:
// a replacement for UILabel
class TextViewLabel: UITextView {
public var numberOfLines: Int = 0 {
didSet {
textContainer.maximumNumberOfLines = numberOfLines
}
}
public var lineBreakMode: NSLineBreakMode = .byTruncatingTail {
didSet {
textContainer.lineBreakMode = lineBreakMode
}
}
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() -> Void {
isScrollEnabled = false
isEditable = false
isSelectable = false
textContainerInset = UIEdgeInsets.zero
textContainer.lineFragmentPadding = 0
}
}
class ViewController: UIViewController {
let labelStackView = UIStackView()
let textViewStackView = UIStackView()
override func viewDidLoad() {
super.viewDidLoad()
let scrollView = UIScrollView()
let hStack = UIStackView()
hStack.axis = .horizontal
hStack.spacing = 8
hStack.distribution = .fillEqually
hStack.alignment = .top
[labelStackView, textViewStackView].forEach { v in
v.axis = .vertical
v.spacing = 4
hStack.addArrangedSubview(v)
}
hStack.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(hStack)
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
let g = view.safeAreaLayoutGuide
let cg = scrollView.contentLayoutGuide
let fg = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
hStack.topAnchor.constraint(equalTo: cg.topAnchor, constant: 8.0),
hStack.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 8.0),
hStack.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: -8.0),
hStack.bottomAnchor.constraint(equalTo: cg.bottomAnchor, constant: -8.0),
hStack.widthAnchor.constraint(equalTo: fg.widthAnchor, constant: -16.0),
])
let infoFont: UIFont = .italicSystemFont(ofSize: 10.0)
let testFont: UIFont = .systemFont(ofSize: 12.0)
var testStr: String = "This is some example text to test truncation consistency. At the end of this line, is a pair of newline chars:\n\nA HEADLINE\n\nThe headline was also followed by a pair of newline chars."
for i in 3...10 {
let infoLabel = UILabel()
infoLabel.font = infoFont
infoLabel.text = "UILabel - numLines: \(i)"
labelStackView.addArrangedSubview(infoLabel)
let v = UILabel()
v.font = testFont
v.numberOfLines = i
v.text = testStr
v.backgroundColor = .yellow
v.lineBreakMode = .byTruncatingTail
labelStackView.addArrangedSubview(v)
labelStackView.setCustomSpacing(16.0, after: v)
}
for i in 3...10 {
let infoLabel = UILabel()
infoLabel.font = infoFont
infoLabel.text = "TextViewLabel - numLines: \(i)"
textViewStackView.addArrangedSubview(infoLabel)
let v = TextViewLabel()
v.font = testFont
v.numberOfLines = i
v.text = testStr
v.backgroundColor = .cyan
v.lineBreakMode = .byTruncatingTail
textViewStackView.addArrangedSubview(v)
textViewStackView.setCustomSpacing(16.0, after: v)
}
scrollView.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
}
}
Differences also exist from one iOS version to another - for example:
It looks like the strings that work only have one paragraph of text, whereas the ones that do not have multiple. Apple's documentation implies that lineBreakMode applies to the label's whole attributedText, as long as it's set after the attributedText is assigned a value. That doesn't explain this behavior though, and the fact that parts of the string are being repeated makes me think this is a bug on how multi-paragraph NSAttributedStrings interact with a label's lineBreakMode.
NSMutableParagraphStyle has a property called lineBreakMode, which will allow you to set the line break mode for each paragraph individually. That might give you a few different options for workarounds.
Alternatively - setting the numberOfLines to 0 also fixes the problem for me, though it's not ideal since it might require you to set a max height for the label, which could worsen the user experience with dynamic font sizes.
Viel Glück!

iOS -How to To Set Anchors to Adjust Spacing for Line Break with Self Sizing CollectionView Cells

When adjusting a label's text for self seizing collectionView cells I use the following function in sizeForItem:
func estimatedLabelHeight(text: String, width: CGFloat, font: UIFont) -> CGFloat {
let size = CGSize(width: width, height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
let attributes = [NSAttributedStringKey.font: font]
let rectangleHeight = String(text).boundingRect(with: size, options: options, attributes: attributes, context: nil).height
return rectangleHeight
}
The function works fine and my cells expand accordingly.
What has happened though is there are several line breaks inside some of the text from the pricesLabel that get fed into the function. The line breaks are too "tight" so I followed this answer when creating my label to expand the spacing in between the line breaks.
let attributedString = NSMutableAttributedString(string: pricesText)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 7 // if I set this at 2 I have no problems
attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, attributedString.length))
The problem is the text from pricesLabel inside the cell expands to far downwards. Using paragraphStyle.lineSpacing = 7 creates the space I want but it causes problems. If I set this to paragraphStyle.lineSpacing = 2 have no problems but the spacing is to tight.
As you can see in the picture the cell sizes the way it's supposed to but the the line break spacing in between the $8.00 and $12.00 makes the text expand to far and the text of $20.00 from the computedTotalLabel gets obscured.
I called sizeToFit() in layoutSubViews() but it made no difference:
override func layoutSubviews() {
super.layoutSubviews()
pricesLabel.sizeToFit()
computedTotalLabel.sizeToFit()
}
How can I make the pricesLabel text adjusted with the line breaks size itself accordingly
class MyCell: UICollectionViewCell {
let pricesLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textAlignment = .right
label.sizeToFit()
label.font = UIFont.systemFont(ofSize: 15.5)
label.adjustsFontSizeToFitWidth = true
label.minimumScaleFactor = 0.5
label.numberOfLines = 0
label.sizeToFit()
return label
}()
let computedTotalLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textAlignment = .right
label.textColor = .black
label.sizeToFit()
label.font = UIFont.boldSystemFont(ofSize: 15.5)
label.adjustsFontSizeToFitWidth = true
label.minimumScaleFactor = 0.5
label.numberOfLines = 1
label.sizeToFit()
return label
}()
let staticTotalLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "Total"
label.textAlignment = .left
label.textColor = .black
label.font = UIFont.boldSystemFont(ofSize: 15.5)
label.adjustsFontSizeToFitWidth = true
label.minimumScaleFactor = 0.5
label.numberOfLines = 1
label.sizeToFit()
return label
}()
let separatorLine: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .lightGray
return view
}()
override func layoutSubviews() {
super.layoutSubviews()
pricesLabel.sizeToFit()
computedTotalLabel.sizeToFit()
}
var myObject: MyObject? {
didSet {
// text is "$8.00\n$12.00\n"
let pricesText = myObject?.myText ?? "error"
let attributedString = NSMutableAttributedString(string: pricesText, attributes: [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 15.5)])
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 7
attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, attributedString.length))
pricesLabel.attributedText = attributedString
computedTotalLabel.text = functionThatTalliesUpAllThePrices(pricesText)
configureAnchors()
}
}
func configureAnchors() {
addSubview(pricesLabel)
addSubview(totalLabel)
addSubview(staticTotalLabel) // this is the label on the left side of the pic that says Total:
addSubview(separatorLine)
pricesLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 12).isActive = true
pricesLabel.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -10).isActive = true
staticTotalLabel.lastBaselineAnchor.constraint(equalTo: totalLabel.lastBaselineAnchor).isActive = true
staticTotalLabel.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 10).isActive = true
staticTotalLabel.rightAnchor.constraint(equalTo: totalLabel.leftAnchor, constant: -10).isActive = true
computedTotalLabel.topAnchor.constraint(equalTo: pricesLabel.bottomAnchor, constant: 0).isActive = true
computedTotalLabel.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -10).isActive = true
separatorLine.topAnchor.constraint(equalTo: computedTotalLabel.bottomAnchor, constant: 12).isActive = true
separatorLine.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 10).isActive = true
separatorLine.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -10).isActive = true
separatorLine.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
separatorLine.heightAnchor.constraint(equalToConstant: 1).isActive = true
}
}
This is the sizeForItem inside the collectionView cell. Not sure if this makes a difference to the problem so I added it anyway
class MyClass: UIViewController {
let tableData = [MyObect]()
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let myObect = tableData[indexPath.item]
// text is "$8.00\n$12.00\n"
let pricesText = myObject?.myText ?? "error"
let width = collectionView.frame.width
let pricesLabelHeight = estimatedLabelHeight(text: pricesText, width: width, font: UIFont.systemFont(ofSize: 15.5))
let total = functionThatTalliesUpAllThePrices(pricesText)
let totalLabelHeight = estimatedLabelHeight(text: functionThatAddsUp, width: width, font: UIFont.boldSystemFont(ofSize: 15.5))
// the 12 + 0 + 12 + 1 are the constant sizes I use inside the cell's configureAnchors functions
let cellHeight = 12 + pricesLabelHeight + 0 + totalLabelHeight + 12 + 1
return CGSize(width: width, height: ceil(cellHeight))
}
}
1st. I had to place the same estimatedLabelHeight(text: String, width: CGFloat, font: UIFont) inside the collectionView cell itself.
2nd. Inside the configureAnchors functions at the bottom of it I call pricesLabel.sizeToFit() and pricesLabel.layoutSubviews() and then I call the above function from step 1 to get the height of the pricesLabel from it's text.
3rd. I set the pricesLabel.heightAnchor.constraint(equalToConstant:) to the the height returned from step 2.
class MyCell: UICollectionViewCell {
// step 1. place this function inside the collectionView cell
func estimatedLabelHeight(text: String, width: CGFloat, font: UIFont) -> CGFloat {
let size = CGSize(width: width, height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union([.usesLineFragmentOrigin, .usesFontLeading])
let attributes = [NSAttributedStringKey.font: font]
let rectangleHeight = String(text).boundingRect(with: size, options: options, attributes: attributes, context: nil).height
return rectangleHeight
}
func configureAnchors() {
// all the other anchors are here
pricesLabel.sizeToFit()
computedTotalLabel.sizeToFit()
computedTotalLabel.layoutIfNeeded()
pricesLabel.layoutIfNeeded()
let pricesLabelText = pricesLabel.text ?? "error"
let width = self.frame.width
// step 2.
let pricesLabelHeight = estimatedLabelHeight(text: pricesLabelText, width: width, font: UIFont.systemFont(ofSize: 15.5))
// step 3.
pricesLabel.heightAnchor.constraint(equalToConstant: pricesLabelHeight).isActive = true
}
}

How to send UIVisualEffectView behind UILabel when based on the label's text's width and height

I added a blur effect behind a label. The blur refuses to go behind the label. I tried all 3 of these separately:
label.insertSubview(backgroundBlur, at: 0)
label.addSubview(backgroundBlur)
label.sendSubview(toBack: backgroundBlur)
The thing is I need the width and height of the UIVisualEffectView blur to be based on the the size of the label's text. The text is dynamic. Both labels need to have their own individual backgroundBlur.
How can I get the UIVisualEffectView blur to go behind each indivdual label when it's also based on the label's text's width and height? There should be two labels with 2 backgroundBlurs behind them.
let backgroundBlur: UIVisualEffectView = {
let blur = UIVisualEffectView(effect: UIBlurEffect(style: UIBlurEffectStyle.dark))
blur.layer.cornerRadius = 6
blur.layer.masksToBounds = true
return blur
}()
let labelOne: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.boldSystemFont(ofSize: 17)
label.textColor = UIColor.white
label.textAlignment = .center
label.sizeToFit()
label.numberOfLines = 0
return label
}()
let labelTwo: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.boldSystemFont(ofSize: 17)
label.textColor = UIColor.white
label.textAlignment = .center
label.sizeToFit()
label.numberOfLines = 0
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(labelOne)
view.addSubview(labelTwo)
labelOne.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
labelOne.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
labelTwo.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
labelTwo.topAnchor.constraint(equalTo: labelOne.bottomAnchor, constant: 16).isActive = true
putBlurEffectBehindLabel(backgroundBlur, labelOne)
putBlurEffectBehindLabel(backgroundBlur, labelTwo)
}
func putBlurEffectBehindLabel(_ blur: UIVisualEffectView, _ label: UILabel){
blur.frame = label.bounds
// tried these individually but nada
label.insertSubview(backgroundBlur, at: 0)
label.addSubview(backgroundBlur)
label.sendSubview(toBack: backgroundBlur)
blur.center = CGPoint(x: label.bounds.midX, y: label.bounds.midY)
}
you have to add UILabel to UIVisualEffectView
backgroundBlur.addSubview(labelOne)
backgroundBlur.addSubview(labelTwo)
add backgroundBlur and set constraint
then add labelOne,labelTwo to backgroundBlur with there constraint
Or Add all to UIView and connect them with constraint
don't forget translatesAutoresizingMaskIntoConstraints = false
Declaration:
let backgroundBlur: UIVisualEffectView = {
let blur = UIVisualEffectView(effect: UIBlurEffect(style:
UIBlurEffectStyle.dark))
blur.layer.cornerRadius = 6
blur.layer.masksToBounds = true
blur.translatesAutoresizingMaskIntoConstraints = false
return blur
}()
let labelOne: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let labelTwo: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
ViewDidLoad
self.view.addSubview(backgroundBlur)
self.view.addSubview(labelOne)
self.view.addSubview(labelTwo)
labelOne.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
labelOne.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
labelTwo.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
labelTwo.topAnchor.constraint(equalTo: labelOne.bottomAnchor, constant: 16).isActive = true
labelOne.text = "asdhaskdas saldhlsahdsa laskdhsakhd asdlhkhsad"
labelTwo.text = "asdhaskdas saldhlsahdsa laskdhsakhd asdlhkhsad"
labelOne.numberOfLines = 0
labelTwo.numberOfLines = 0
backgroundBlur.topAnchor.constraint(equalTo: labelOne.topAnchor, constant: -8).isActive = true
backgroundBlur.bottomAnchor.constraint(equalTo: labelTwo.bottomAnchor, constant: 8).isActive = true
backgroundBlur.trailingAnchor.constraint(equalTo: labelOne.trailingAnchor, constant: 8).isActive = true
backgroundBlur.leadingAnchor.constraint(equalTo: labelOne.leadingAnchor, constant: -8).isActive = true
I had to add two UIVisualEffectView named:
backgroundBlurOne
backgroundBlurTwo
Based on #AbdelahadDarwish suggestion of adding the label to the blur instead of adding the blur to the label I was able to get the blur behind the label:
// this is added inside the putBlurEffectBehindLabel function
blur.contentView.addSubview(label)
Also inside the putBlurEffectBehindLabel function I got the size of the label's text using (text! as NSString).size(withAttributes: [NSAttributedStringKey.font: UIFont])
and then based the width and height of the UIVisualEffectView (the blur) off of that.
I then added the text I wanted for labelOne and backgroundBlurOne in viewDidLoad.
Then I added the text I wanted for labelTwo and the backgroundBlurTwo for it in viewDidAppear. I had to do that so I can use the height from backgroundBlurOne + a 16 point distance so labelTwo could be where I needed it to be from labelOne.
let backgroundBlurOne: UIVisualEffectView = {
let blur = UIVisualEffectView(effect: UIBlurEffect(style: UIBlurEffectStyle.dark))
blur.translatesAutoresizingMaskIntoConstraints = false
blur.layer.cornerRadius = 6
blur.layer.masksToBounds = true
blur.isUserInteractionEnabled = false
blur.backgroundColor = UIColor.black.withAlphaComponent(10)
return blur
}()
let backgroundBlurTwo: UIVisualEffectView = {
let blur = UIVisualEffectView(effect: UIBlurEffect(style: UIBlurEffectStyle.dark))
blur.translatesAutoresizingMaskIntoConstraints = false
blur.layer.cornerRadius = 6
blur.layer.masksToBounds = true
blur.isUserInteractionEnabled = false
blur.backgroundColor = UIColor.black.withAlphaComponent(10)
return blur
}()
let labelOne: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.systemFont(ofSize: 17)
label.textColor = UIColor.white
label.textAlignment = .center
label.sizeToFit()
label.numberOfLines = 0
return label
}()
let labelTwo: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.systemFont(ofSize: 17)
label.textColor = UIColor.white
label.textAlignment = .center
label.sizeToFit()
label.numberOfLines = 0
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
labelOne.text = "Hello"
putBlurEffectBehindLabel(backgroundBlurOne, labelOne, yDistance: 0)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
labelTwo.text = "Heloooooooooooooooooooooooo"
putBlurEffectBehindLabel(backgroundBlurTwo, labelTwo, yDistance: backgroundBlurOne.frame.height + 16)
}
func putBlurEffectBehindLabel(_ blur: UIVisualEffectView, _ label: UILabel, yDistance: CGFloat){
view.addSubview(blur)
blur.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
blur.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: yDistance).isActive = true
blur.contentView.addSubview(label)
label.centerXAnchor.constraint(equalTo: blur.centerXAnchor).isActive = true
label.centerYAnchor.constraint(equalTo: blur.centerYAnchor).isActive = true
let text = label.text
let textSize = (text! as NSString).size(withAttributes: [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 17)])
blur.widthAnchor.constraint(equalToConstant: textSize.width + 15).isActive = true
blur.heightAnchor.constraint(equalToConstant: textSize.height + 10).isActive = true
}

Wrapping an iOS UILabel as a block within a constrained UIView

I have a couple of UILabels within an UIView.
I constrain that containing view to 100px. If both the UILabels have an intrinsic width of 75px each (because of their content) what I would like is that the second label drops below the first because it cannot display without wrapping it's own text.
Is there a containing View in iOS that would support that behaviour?
Here is one example of "wrapping" labels based on fitting into the width of a parent view.
You can run this directly in a Playground page... Tap the red "Tap Me" button to toggle the text in the labels, to see how they "fit".
import UIKit
import PlaygroundSupport
// String extension for easy text width/height calculations
extension String {
func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [.font: font], context: nil)
return ceil(boundingBox.height)
}
func width(withConstrainedHeight height: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [.font: font], context: nil)
return ceil(boundingBox.width)
}
}
class TestViewController : UIViewController {
let btn: UIButton = {
let b = UIButton()
b.translatesAutoresizingMaskIntoConstraints = false
b.setTitle("Tap Me", for: .normal)
b.backgroundColor = .red
return b
}()
let cView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .blue
return v
}()
let labelA: UILabel = {
let v = UILabel()
// we will be explicitly setting the label's frame
v.translatesAutoresizingMaskIntoConstraints = true
v.backgroundColor = .yellow
return v
}()
let labelB: UILabel = {
let v = UILabel()
// we will be explicitly setting the label's frame
v.translatesAutoresizingMaskIntoConstraints = true
v.backgroundColor = .cyan
return v
}()
// spacing between labels when both fit on one line
let spacing: CGFloat = 8.0
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(btn)
btn.addTarget(self, action: #selector(didTap(_:)), for: .touchUpInside)
btn.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
btn.topAnchor.constraint(equalTo: view.topAnchor, constant: 20.0).isActive = true
// add the "containing" view
view.addSubview(cView)
// add the two labels to the containing view
cView.addSubview(labelA)
cView.addSubview(labelB)
// constrain containing view Left-20, Top-20 (below the button)
cView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20.0).isActive = true
cView.topAnchor.constraint(equalTo: btn.bottomAnchor, constant: 20.0).isActive = true
// containing view has a fixed width of 100
cView.widthAnchor.constraint(equalToConstant: 100.0).isActive = true
// constrain bottom of containing view to bottom of labelB (so the height auto-sizes)
cView.bottomAnchor.constraint(equalTo: labelB.bottomAnchor, constant: 0.0).isActive = true
// initial text in the labels - both will fit "on one line"
labelA.text = "First"
labelB.text = "Short"
}
func updateLabels() -> Void {
// get the label height based on its font
if let h = labelA.text?.height(withConstrainedWidth: CGFloat.greatestFiniteMagnitude, font: labelA.font) {
// get the calculated width of each label
if let wA = labelA.text?.width(withConstrainedHeight: h, font: labelA.font),
let wB = labelB.text?.width(withConstrainedHeight: h, font: labelB.font) {
// labelA frame will always start at 0,0
labelA.frame = CGRect(x: 0.0, y: 0.0, width: wA, height: h)
// will both labels + spacing fit in the containing view's width?
if wA + wB + spacing <= cView.frame.size.width {
// yes, so place labelB to the right of labelA (put them on "one line")
labelB.frame = CGRect(x: wA + spacing, y: 0.0, width: wB, height: h)
} else {
// no, so place labelB below labelA ("wrap" labelB "to the next line")
labelB.frame = CGRect(x: 0.0, y: h, width: wB, height: h)
}
}
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
updateLabels()
}
#objc func didTap(_ sender: Any?) -> Void {
// toggle labelB's text
if labelB.text == "Short" {
labelB.text = "Longer text"
} else {
labelB.text = "Short"
}
// adjust size / position of labels
updateLabels()
}
}
let vc = TestViewController()
vc.view.backgroundColor = .white
PlaygroundPage.current.liveView = vc

Resources