I am trying to create a UILabel which contains two texts of different fonts where one NSMutableAttributedString sits vertically on top of the other. Upon attempting to insert a line break via swift's \n I found that the appended string disappears. I have tried a variety of lineBreakModes with no result (with and without \n) along with ensuring the frame isnt constricting the texts visibility by setting a large maximumLineHeight.
I should also mention that according to Apple's documentation when setting UILabel.attributedText to any NSAttributedText instance
When the label has an attributed string value, the system ignores the textColor, font, textAlignment, lineBreakMode, and lineBreakStrategy properties. Set the foregroundColor, font, alignment, lineBreakMode, and lineBreakStrategy properties in the attributed string instead.
Here is some of the code simplified for the sake of the question (I have also tried calling .sizeTofit() after setting the labels attributedText as well as setting different .lineBreakStrategys)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
paragraphStyle.lineBreakMode = .byWordWrapping
let totalVisitsString = NSMutableAttributedString(string: "\(visitLogs.count)\n", attributes: [.font : UIFont.systemFont(ofSize: 25), .paragraphStyle : paragraphStyle])
totalVisitsString.append(NSMutableAttributedString(string: "Total visits", attributes: [.font : UIFont.systemFont(ofSize: 14)]))
totalVisitsLabel.attributedText = totalVisitsString
the label itself:
var totalVisitsLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
By default your label's numberOfLines is 1. You never change it so that is what you get.
Labels when created are defaulted to 1 line. You'll need to set the number of lines to 0 (unlimited) or whatever number you want to max it at.
var totalVisitsLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
I am drawing text within a PDF using this method:
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
paragraphStyle.lineBreakMode = .byWordWrapping
let textAttributes = [
NSAttributedString.Key.paragraphStyle: paragraphStyle,
NSAttributedString.Key.font: UIFont(name: "MySpecialFont", size: 16)!,
NSAttributedString.Key.foregroundColor: UIColor.black
]
let attributedText = NSAttributedString(
string: myTextArray.first,
attributes: textAttributes
)
let textRect = CGRect(
x: 400,
y: 40,
width: 80,
height: 40
)
attributedText.draw(in: textRect)
Above draws the text fine. However, sometimes, the string passed on to it seem to be too long and go on for 3 lines instead of 2. In these cases, I want to textRect to be taller. Basically to know how many lines it would take so textRect could be adjusted.
There are several functions within NSAttributedString that gives string length, but thats the length if it was in a single line.
Is there a way to know how many lines the final attributedText would take inside textRect?
Use NSAttributedString.boundingRect(with:options:context:) to compute the size required. You should pass .usesLineFragmentOrigin as an option so that it'll compute it for multiple lines. Pass the width you want; the height doesn't really matter, because it'll expand the height to contain the full string, and you can use that to work out your final rectangle.
That said, from your description, it sounds like you can just make the height very large (10,000 is the usual value; but maybe you want to just give room for three lines). Since you're just drawing here; it shouldn't matter if the available rect is taller than required. It only matters if it's shorter.
I'm trying to implement by-design labels coming from Sketch e.g. I need text styles with font size = 19 and line height = 50. So I ended up using NSAttributedString with NSMutableParagraphStyle but was stopped by problem with text being sticked to bottom of UILabel
I've already tried to use lineHeightMultiple and lineSpacing but those didn't give me the line height I wanted so I ended up using minimumLineHeight and maximumLineHeight equal the same
Here is my approach to make NSAttributedString
private static func makeAttributedString(
with attributes: TextAttributes,
text: String? = nil,
alignment: NSTextAlignment = .center
) -> NSAttributedString {
let font = UIFont(name: attributes.font.rawValue, size: attributes.fontSize)!
let paragraph = NSMutableParagraphStyle()
paragraph.alignment = alignment
paragraph.paragraphSpacing = attributes.paragraph
paragraph.minimumLineHeight = attributes.lineHeight // equal 50 in my case
paragraph.maximumLineHeight = attributes.lineHeight // equal 50 in my case
let attributes: [NSAttributedStringKey: Any] = [
NSAttributedStringKey.paragraphStyle: paragraph,
NSAttributedStringKey.foregroundColor: attributes.textColor,
NSAttributedStringKey.kern: attributes.kern,
NSAttributedStringKey.font: font
]
return NSAttributedString(string: text ?? "", attributes: attributes)
}
I expect result similar to design
but actually getting
Note: setting height constraint to 50 is not applicable because I also need multiline labels but there is the same bug with them
Seems like I've found some workaround myself, maybe it will help someone.
The method is about setting baselineOffset like this:
NSAttributedStringKey.baselineOffset: (attributes.lineHeight - font.lineHeight) / 4
Works like charm:
https://i.imgur.com/a2EOf5R.png
When I draw an attributed string with a fixed line height with Text Kit, the characters always get aligned to the bottom of the line fragment. While this would make sense on one line with characters varying in size, this breaks the flow of the text with multiple lines. The baselines appear decided by the largest descender for each line.
I've found an article from the people behind Sketch explaining this exact problem in a bit more detail and showing what their solution does, but obviously not explaining how they achieved this.
This is what I want basically:
When showing two lines with a large line height, this result is far from ideal:
The code I'm using:
let smallFont = UIFont.systemFont(ofSize: 15)
let bigFont = UIFont.systemFont(ofSize: 25)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.minimumLineHeight = 22
paragraphStyle.maximumLineHeight = 22
var attributes = [
NSFontAttributeName: smallFont,
NSParagraphStyleAttributeName: paragraphStyle
]
let textStorage = NSTextStorage()
let textContainer = NSTextContainer(size: CGSize(width: 250, height: 500))
let layoutManager = NSLayoutManager()
textStorage.append(NSAttributedString(string: "It is a long established fact that a reader will be ", attributes:attributes))
attributes[NSFontAttributeName] = bigFont
textStorage.append(NSAttributedString(string: "distracted", attributes:attributes))
attributes[NSFontAttributeName] = smallFont
textStorage.append(NSAttributedString(string: " by the readable content of a page when looking at its layout.", attributes:attributes))
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
let textView = UITextView(frame: self.view.bounds, textContainer:textContainer)
view.addSubview(textView)
I managed to get this working, but had to drop support for iOS 8 and macOS 10.10 unfortunately.
If you implement the following delegate call of the NSLayoutManager, you get to decide what to do with the baselineOffset for each line fragment:
optional func layoutManager(_ layoutManager: NSLayoutManager,
shouldSetLineFragmentRect lineFragmentRect: UnsafeMutablePointer<CGRect>,
lineFragmentUsedRect: UnsafeMutablePointer<CGRect>,
baselineOffset: UnsafeMutablePointer<CGFloat>,
in textContainer: NSTextContainer,
forGlyphRange glyphRange: NSRange) -> Bool
When the NSTextStorage is created and for each subsequent change, I enumerate all used font, calculate it's default line height (NSLayoutManager.defaultLineHeightForFont()) and store the biggest line height. In the implementation of the above mentioned delegate method I check the current line height of the NSParagraphStyle for the provided line fragment and align the font's line height within that value. From there the baseline offset can be calculated with the knowledge that the baseline sits between the font's ascender and descender. Update the baselineOffset value with baselineOffset.memory(newOffset) and everything should be aligned as you'd like.
Note: I'm not going in too much detail about the actual code used to implement this because I'm not sure I'm using the right values throughout these calculations. I might update this in the near future when the whole approach is tried and proven.
Update: Implementation of adjusting baseline. Every time the textContainer changes I recalculate the biggest line height and biggest descender. Then I basically do this in the layout manager's delegate function:
var baseline: CGFloat = (lineFragmentRect.pointee.height - biggestLineHeight) / 2
baseline += biggestLineHeight
baseline -= biggestDescender
baseline = min(max(baseline, 0), lineFragmentRect.pointee.height)
baselineOffset.pointee = floor(baseline)
I'm trying to create a messaging application and am encountering a very strange issue.
The reason there is so much space between "Thomas" and the bottom of the text bubble is becasue the UILabel is creating another line. Currently I'm setting the label's text using the attributedText property, and passing in a NSMutableParagraphStyle with a line spacing of 8. If I set the line spacing to 0, the space between "Thomas" and the bottom of the text bubble goes away like so:
Here's where it gets strange though. If I set the paragraph line spacing back to 8, and add a couple more characters to the line, the text bubble appears without the extra line:
All help is greatly appreciated :)
Here is my code:
class MessageTableViewCell: UITableViewCell {
var didSetupConstraints = false
var thumbnail = UIImageView.newAutoLayoutView()
let messageTailIcon = UIImageView.newAutoLayoutView()
var messageView = UIView.newAutoLayoutView()
var messageLabel = UILabel.newAutoLayoutView()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupViews()
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupViews() {
thumbnail.image = UIImage(named: "ThomasBaldwin")
thumbnail.layer.cornerRadius = 17.5
thumbnail.clipsToBounds = true
messageTailIcon.image = UIImage(named: "MessageTailIcon")
messageView.backgroundColor = Application.greyColor
messageView.layer.cornerRadius = 10
let paragraphStyle = NSMutableParagraphStyle.new()
paragraphStyle.lineSpacing = 8
messageLabel.numberOfLines = 0
messageLabel.layer.cornerRadius = 10
messageLabel.attributedText = NSMutableAttributedString(
string: "Thomas says hello",
attributes: [
NSFontAttributeName: UIFont(name: "AvenirNextLTPro-Regular", size: 12.5)!,
NSForegroundColorAttributeName: UIColor.colorFromCode(0x262626),
NSBackgroundColorAttributeName: Application.greyColor,
NSKernAttributeName: 0.5,
NSParagraphStyleAttributeName: paragraphStyle
]
)
contentView.addSubview(thumbnail)
contentView.addSubview(messageView)
messageView.addSubview(messageTailIcon)
messageView.addSubview(messageLabel)
updateConstraints()
}
override func updateConstraints() {
if !didSetupConstraints {
thumbnail.autoPinEdgeToSuperviewEdge(.Top, withInset: 15)
thumbnail.autoPinEdgeToSuperviewEdge(.Leading, withInset: 8.5)
thumbnail.autoSetDimensionsToSize(CGSize(width: 35, height: 35))
messageView.autoPinEdgeToSuperviewEdge(.Top, withInset: 17.5)
messageView.autoPinEdge(.Leading, toEdge: .Trailing, ofView: thumbnail, withOffset: 10)
messageView.autoPinEdgeToSuperviewEdge(.Trailing, withInset: 24.5)
messageView.autoPinEdgeToSuperviewEdge(.Bottom)
messageTailIcon.autoPinEdgeToSuperviewEdge(.Top, withInset: 15)
messageTailIcon.autoPinEdgeToSuperviewEdge(.Leading, withInset: -10)
messageTailIcon.autoSetDimensionsToSize(CGSize(width: 18, height: 9))
messageLabel.autoPinEdgesToSuperviewEdgesWithInsets(UIEdgeInsets(top: 8.5, left: 10, bottom: 8.5, right: 5), excludingEdge: .Trailing)
messageLabel.autoPinEdgeToSuperviewEdge(.Trailing, withInset: 0, relation: .GreaterThanOrEqual)
didSetupConstraints = true
}
super.updateConstraints()
}
}
If you would like to view a sample project demonstrating the issue, I've pushed one to github
Okay, so finally locked down and easy answer, this has to do with your KERNING attributed only. watch this:
Also disregard the sizing of the red cell, this is NOT happening like this in the app, this is just a product of my screenshots being different sizes, but please do try this for yourself. Comment out the kerning and the reapply it and you'll see the same thing
with kerning with "Thomas"
without kerning with "Thomas"
with kerning with "Thomas says hello"
without kerning with "Thomas says hello"
I've done everything possible to check the code, use different constraints, and I even played around with ALL options of an NSAttributedString, and the only thing that changes this bad behavior is the kerning attribute, and it's doing this to all types of fonts, not just Avenir. In fact, the font you used in this example is system font when you didn't set a font at all, but I've tried it with 3 fonts now, same effect, the kerning seems to be broken or perhaps its working as intended for Swift and/or ObjC, but I think this is actually a bug.
Most NSAttributedString options, if you want to mess around with stuff:
var myString1 = NSMutableAttributedString(string:"Thomas asdfadsf asdfasdfasdf asdfasdf asdfasdf \n asdfasdf asdf \n")
let myString1Font1 = UIFont.systemFontOfSize(12.0)
let originalNSString = myString1.string as NSString
let myString1Range1 = originalNSString.rangeOfString(myString1.string)
var myString1ParaStyle1 = NSMutableParagraphStyle()
myString1ParaStyle1.alignment = NSTextAlignment.Natural
myString1ParaStyle1.baseWritingDirection = NSWritingDirection.Natural
myString1ParaStyle1.defaultTabInterval = 0.0
myString1ParaStyle1.firstLineHeadIndent = 0.0
myString1ParaStyle1.headIndent = 0.0
myString1ParaStyle1.hyphenationFactor = 0.0
myString1ParaStyle1.lineBreakMode = NSLineBreakMode.ByWordWrapping
myString1ParaStyle1.lineHeightMultiple = 0.0
myString1ParaStyle1.lineSpacing = 8.0
myString1ParaStyle1.maximumLineHeight = 0.0
myString1ParaStyle1.minimumLineHeight = 0.0
myString1ParaStyle1.paragraphSpacing = 0.0
myString1ParaStyle1.paragraphSpacingBefore = 0.0
myString1ParaStyle1.tailIndent = 0.0
myString1.addAttribute(NSKernAttributeName, value:0.5, range:myString1Range1)
myString1.addAttribute(NSFontAttributeName, value:myString1Font1, range:myString1Range1)
myString1.addAttribute(NSParagraphStyleAttributeName, value:myString1ParaStyle1, range:myString1Range1)
myString1.addAttribute(NSBackgroundColorAttributeName, value:UIColor.redColor(), range:myString1Range1)
myString1.addAttribute(NSForegroundColorAttributeName, value:UIColor.blackColor(), range:myString1Range1)
Again, this isn't an constraints issue, I was wrong, this is only a KERNING issue, and this sucks, but such is life, perhaps this needs to be reported to RADAR.
Also, you can try this for yourself, anything BUT a 0 or 0.00000 or as many zeros as you want will produce the wrong results with Kerning, i tried this and it messes up your label field the same way that kerning would mess up the field with a larger value:
NSKernAttributeName: 0.00000000000001
HOLD up, I solved it, from what it looks like, set this value, just add this to your paragraphStyle variable that you set up in the example project, its working with the kerning, not sure if this is working for all fonts, but it fixes your example project at least:
paragraphStyle.lineHeightMultiple = 1.5
The only problem with this method is that it works for lines with one word or one line, you'll have to do a word count adjustment to set this "lineHeightMultiple" based on when a new line appears, this sucks, but it works, obviously, not a very good method to use, but works if you have a 1 liner string, needs adjusting if you have more, otherwise just turn off kerning, and it will be solved wihout this line height multiple.
It's as if the line height is changing internally and pushing characters to a new line but apple isn't automatically accounting for this change in character width.
And as a matter of fact, I think the answer you are looking for isn't kerning at all but tracking, which will push the letters apart from each other. The problem with kerning is that kerning screws around with the glyphs of the fonts and overrides some of their behaviors and as such it can be anoticeable effect like we are seeeing here.
From Apple:
preferredFontForTextStyle:, the specific font returned includes traits
which vary according to user preferences and context, including
tracking (letter-spacing) adjustments, in addition to being tuned for
the use specified by the particular text style constant. The fonts
returned using text style constants are meant to be used for all text
in an app other than text in user interface elements, such as buttons,
bars, and labels. Naturally, you need to choose text styles that look
right in your app. It’s also important to observe the
UIContentSizeCategoryDidChangeNotification so that you can re–lay out
the text when the user changes the content size category. When your
app receives that notification, it should send the
invalidateIntrinsicContentSize message to views positioned by Auto
Layout or send setNeedsLayout to user interface elements positioned
manually. And it should invalidate preferred fonts or font descriptors
and acquire new ones as needed.
https://developer.apple.com/library/ios/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/TypoFeatures/TextSystemFeatures.html
If you truly need kerning, then you should probably be tweaking the kerning values of the ligatures if the font has any available to play with.
Other things to consider, this does work, but it's also bold so it's already not something that matches your style above, but it's something you can toy around with:
let sytemDynamicFontDescriptor = UIFontDescriptor.preferredFontDescriptorWithTextStyle(UIFontTextStyleHeadline)
let size = sytemDynamicFontDescriptor.pointSize
let myString1Font1 = UIFont(descriptor: sytemDynamicFontDescriptor, size:size)
println(sytemDynamicFontDescriptor.fontAttributes())
messageLabel.numberOfLines = 0
messageLabel.layer.cornerRadius = 10
messageLabel.attributedText = NSMutableAttributedString(
string: "Thomas asdfad as ",
// string: "Thomas says hello", // switch back to this to see it display the text properly on one
attributes: [
NSFontAttributeName: myString1Font1,
NSForegroundColorAttributeName: UIColor.blackColor(),
NSBackgroundColorAttributeName: UIColor.redColor(),
NSKernAttributeName: 0.5,
NSParagraphStyleAttributeName: paragraphStyle
]
)