I am using a kerning attribute on a UILabel to display its text with some custom letter spacing. Unfortunately, as I'm displaying user-generated strings, I sometimes see things like the following:
ie sometimes some emoji characters are not being displayed.
If I comment out the kerning but apply some paragraph style instead, I get the same kind of errored rendering.
I couldn't find anything in the documentation explicitely rejecting support for special unicode characters. Am I doing something wrong or is it an iOS bug?
The code to reproduce the bug is available as a playground here: https://github.com/Bootstragram/Playgrounds/tree/master/LabelWithEmoji.playground
and copied here:
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
extension NSAttributedString {
static func kernedSpacedText(_ text: String,
letterSpacing: CGFloat = 0.0,
lineHeight: CGFloat? = nil) -> NSAttributedString {
// TODO add the font attribute
let attributedString = NSMutableAttributedString(string: text)
attributedString.addAttribute(NSAttributedStringKey.kern,
value: letterSpacing,
range: NSRange(location: 0, length: text.count))
if let lineHeight = lineHeight {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = lineHeight
attributedString.addAttribute(NSAttributedStringKey.paragraphStyle,
value: paragraphStyle,
range: NSRange(location: 0, length: text.count))
}
return attributedString
}
}
//for familyName in UIFont.familyNames {
// for fontName in UIFont.fontNames(forFamilyName: familyName) {
// print(fontName)
// }
//}
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let myString = "1β½πΊπ»βΎοΈπ―πββοΈπ\n2 ππΏπΈ π»"
let label = UILabel()
label.frame = CGRect(x: 150, y: 200, width: 200, height: 100)
label.attributedText = NSAttributedString.kernedSpacedText(myString)
label.numberOfLines = 0
label.textColor = .black
view.addSubview(label)
self.view = view
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
Thanks.
TL, DR:
String.count != NSString.length. Any time you see NSRange, you must convert your String into UTF-16:
static func kernedSpacedText(_ text: String,
letterSpacing: CGFloat = 0.0,
lineHeight: CGFloat? = nil) -> NSAttributedString {
// TODO add the font attribute
let attributedString = NSMutableAttributedString(string: text)
attributedString.addAttribute(NSAttributedStringKey.kern,
value: letterSpacing,
range: NSRange(location: 0, length: text.utf16.count))
if let lineHeight = lineHeight {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = lineHeight
attributedString.addAttribute(NSAttributedStringKey.paragraphStyle,
value: paragraphStyle,
range: NSRange(location: 0, length: text.utf16.count))
}
return attributedString
}
The longer explanation
Yours is a common problem converting between Swift's String and ObjC's NSString. The length of a String is the number of extended grapheme clusters; in ObjC, it's the number of UTF-16 code points needed to encode that string.
Take the thumb-up character for example:
let str = "π"
let nsStr = str as NSString
print(str.count) // 1
print(nsStr.length) // 2
Things can get even weirder when it comes to the flag emojis:
let str = "πΊπΈ"
let nsStr = str as NSString
print(str.count) // 1
print(nsStr.length) // 4
Even though this article was written all the way back in 2003, it's still a good read today:
The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets.
Related
I have a table view, inside the table view I have multiple cells as shows in attached image.
I want to show amenities items like : air conditioning, swimming pool, gym, tv cable etc like bullets.
How can I achieve this
I have all these value in my model coming from API response.
let ac = "Air Conditioning"
let tv = "TV Cable"
let washing = "Washing Machine"
let connectivity = "Wi-Fi"
let gym = "Gym"
Good case for stack views...
Vertical UIStackView
Each "row" is a Horizontal UIStackView with Distribution: Fill Equally
Looks like this - showing Bounds Rectangles:
without the Bounds Rectangles:
view hierarchy:
Edit
A second approach (which you may find easier)...
Use a single horizontal stack view with a single label on each "side" and use .attributedText.
Set the .attributedText of each label to a formatted, bulleted attributed string.
The cell prototype could look like this:
Then, use this code (slightly modified from https://stackoverflow.com/a/46889728/6257435) to generate a formatted, bulleted attributed string:
func add(bulletList strings: [String],
font: UIFont,
indentation: CGFloat = 15,
lineSpacing: CGFloat = 3,
paragraphSpacing: CGFloat = 10,
textColor: UIColor = .black,
bulletColor: UIColor = .red) -> NSAttributedString {
func createParagraphAttirbute() -> NSParagraphStyle {
var paragraphStyle: NSMutableParagraphStyle
let nonOptions = NSDictionary() as! [NSTextTab.OptionKey: Any]
paragraphStyle = NSParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
paragraphStyle.tabStops = [
NSTextTab(textAlignment: .left, location: indentation, options: nonOptions)]
paragraphStyle.defaultTabInterval = indentation
paragraphStyle.firstLineHeadIndent = 0
paragraphStyle.lineSpacing = lineSpacing
paragraphStyle.paragraphSpacing = paragraphSpacing
paragraphStyle.headIndent = indentation
return paragraphStyle
}
let bulletPoint = "\u{2022}"
let textAttributes: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: textColor]
let bulletAttributes: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: bulletColor]
let buffer = NSMutableAttributedString.init()
for string in strings {
var formattedString = "\(bulletPoint)\t\(string)"
// don't add newLine if it's the last bullet
if string != strings.last {
formattedString += "\n"
}
let attributedString = NSMutableAttributedString(string: formattedString)
let paragraphStyle = createParagraphAttirbute()
attributedString.addAttributes(
[NSAttributedString.Key.paragraphStyle : paragraphStyle],
range: NSMakeRange(0, attributedString.length))
attributedString.addAttributes(
textAttributes,
range: NSMakeRange(0, attributedString.length))
let string:NSString = NSString(string: formattedString)
let rangeForBullet:NSRange = string.range(of: bulletPoint)
attributedString.addAttributes(bulletAttributes, range: rangeForBullet)
buffer.append(attributedString)
}
return buffer
}
A quick implementation of that using your image as sample data results in this:
I have a class called "rectangle" to make custom UILabels. I override "draw" in the rectangle class. When I instantiate the label, I want the FIRST line of text to show up in bolded font. I know how to solve this by manually getting the range for each string... however, I have more than 300 strings to do. The strings are currently in an array, formatted like so: "Happy \n Birthday". How can I make the word "Happy" bold?
var messageText = "Happy \n Birthday"
let rectanglePath = UIBezierPath(rect: rectangleRect)
context.saveGState()
UIColor.white.setFill()
rectanglePath.fill()
context.restoreGState()
darkPurple.setStroke()
rectanglePath.lineWidth = 0.5
rectanglePath.lineCapStyle = .square
rectanglePath.lineJoinStyle = .round
rectanglePath.stroke()
let rectangleStyle = NSMutableParagraphStyle()
rectangleStyle.alignment = .center
let rectangleFontAttributes = [
.font: UIFont.myCustomFont(true),
.foregroundColor: UIColor.black,
.paragraphStyle: rectangleStyle,
] as [NSAttributedString.Key: Any]
let rectangleTextHeight: CGFloat = messageText.boundingRect(with: CGSize(width: rectangleRect.width, height: CGFloat.infinity), options: .usesLineFragmentOrigin, attributes: rectangleFontAttributes, context: nil).height
context.saveGState()
context.clip(to: rectangleRect)
messageText.draw(in: CGRect(x: rectangleRect.minX, y: rectangleRect.minY + (rectangleRect.height - rectangleTextHeight) / 2, width: rectangleRect.width, height: rectangleTextHeight), withAttributes: rectangleFontAttributes)
context.restoreGState()
You can find the first by separating the string by newline:
let firstLine = "Happy \n Birthday".split(separator: "\n").first
This will give you the first line of the string. (long text multi lining doesn't count) then you can find the range using this and apply the bold effect.
How this works:
You need to set the label the way that accepts multiline:
Find the range of first line
Convert it to nsRange
Apply attributes to the range
Here is a fully working example:
import UIKit
import PlaygroundSupport
extension StringProtocol where Index == String.Index {
func nsRange(from range: Range<Index>) -> NSRange {
return NSRange(range, in: self)
}
}
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let label = UILabel()
label.numberOfLines = 0
label.text = "Happy \n Birthday"
label.textColor = .black
let text = "Happy \n Birthday"
let attributedString = NSMutableAttributedString(string: text)
let firstLine = text.split(separator: "\n").first!
let range = text.range(of: firstLine)!
attributedString.addAttributes([.font : UIFont.boldSystemFont(ofSize: 14)], range: text.nsRange(from: range))
label.attributedText = attributedString
label.sizeToFit()
view.addSubview(label)
self.view = view
}
}
PlaygroundPage.current.liveView = MyViewController()
im trying to calculate the height of a UILabel with LineSpacing attribute. The weird thing is that calculated value of the height of the normal label.text is lower then the label.attributedText with its lineheight. it looks like i'm doing something wrong, but cant find what, so please help :D.
The provided code is specially made for SO to make it compact and clear, it is implemented differently in my project.
extension NSAttributedString {
func heightWithWidth(width: CGFloat) -> CGFloat {
let maxSize = CGSize(width: width, height: CGFloat.max)
let boundingBox = self.boundingRectWithSize(maxSize, options: [.UsesLineFragmentOrigin, .UsesFontLeading, .UsesDeviceMetrics], context: nil)
return boundingBox.height
}
}
extension UILabel {
func getHeightWithGivenWidthAndLineHeight(lineHeight: CGFloat, labelWidth: CGFloat) -> CGFloat {
let text = self.text
if let text = text {
let attributeString = NSMutableAttributedString(string: text)
let style = NSMutableParagraphStyle()
style.lineSpacing = lineHeight
attributeString.addAttribute(NSParagraphStyleAttributeName, value: style, range: NSMakeRange(0, text.characters.count))
let height = attributeString.heightWithWidth(labelWidth)
self.attributedText = attributeString
return height
}
return 0
}
I call this by
let contentHeight = contentLabel.text! == "" ? 0 : contentLabel.getHeightWithGivenWidthAndLineHeight(3, labelWidth: labelWidth)
Working with normal strings (without spacing) works perfectly, when i use attributedstring with lineSpacing it fails to calculate the correct value.
You can just use UILabel's sizeThatFits. For example:
let text = "This is\nSome\nGreat\nText"
let contentHeight = contentLabel.text! == "" ? 0 : contentLabel.getHeightWidthGivenWidthAndLineHeight(6, labelWidth: labelWidth)
//returns 73.2
But just setting
contentLabel.attributedText = contentLabel.attributedString //attributedString is same as getHeightWidthGivenWidthAndLineHeight
let size = contentLabel.sizeThatFits(contentLabel.frame.size)
//returns (w 49.5,h 99.5)
Code for attributedString added to your extension, if you need to see that:
var attributedString:NSAttributedString?{
if let text = self.text{
let attributeString = NSMutableAttributedString(string: text)
let style = NSMutableParagraphStyle()
style.lineSpacing = 6
attributeString.addAttribute(NSParagraphStyleAttributeName, value: style, range: NSMakeRange(0, text.characters.count))
return attributeString
}
return nil
}
I updated my Extension this way to set the line height and return the new label height at the same time. Thanx to beyowulf
extension UILabel {
func setLineHeight(lineHeight: CGFloat, labelWidth: CGFloat) -> CGFloat {
let text = self.text
if let text = text {
let attributeString = NSMutableAttributedString(string: text)
let style = NSMutableParagraphStyle()
style.lineSpacing = lineHeight
attributeString.addAttribute(NSParagraphStyleAttributeName, value: style, range: NSMakeRange(0, text.characters.count))
self.attributedText = attributeString
return self.sizeThatFits(CGSize(width: labelWidth, height: 20)).height
}
return 0
}
}
I've been working on a small project using Xcode. It uses a lot of labels, textfields, etc. I've finished with most of the layout, the constrains, and the forms, titles, etc. After which, the client announces that for all required fields, there should be a red asterisk next to the label.
Call me lazy, but I'd rather not go back to all of my forms, add in a lot of labels with asterisks on them, and re-do my auto-layout to accommodate the new labels.
So, is there a way to change the colour of a specific character (in this case, the asterisk) in a UILabel, while the rest of the text stays black?
You can use NSMutableAttributedString.
You can set specific range of your string with different color, font, size, ...
E.g:
var range = NSRange(location:2,length:1) // specific location. This means "range" handle 1 character at location 2
attributedString = NSMutableAttributedString(string: originalString, attributes: [NSFontAttributeName:UIFont(name: "Georgia", size: 18.0)!])
// here you change the character to red color
attributedString.addAttribute(NSForegroundColorAttributeName, value: UIColor.redColor(), range: range)
label.attributedText = attributedString
Ref: Use multiple font colors in a single label - Swift
You can change a lot of attribute of String.
Ref from apple: https://developer.apple.com/library/prerelease/ios/documentation/Cocoa/Reference/Foundation/Classes/NSMutableAttributedString_Class/index.html
let text = "Sample text *"
let range = (text as NSString).rangeOfString("*")
let attributedString = NSMutableAttributedString(string:text)
attributedString.addAttribute(NSForegroundColorAttributeName, value: UIColor.redColor() , range: range)
//Apply to the label
myLabel.attributedText = attributedString;
UILabel have an .attributedText property of type NSAttributedString.
Declaration
#NSCopying var attributedText: NSAttributedString?
You let your asterix * have a single .redColor() attribute (NSForegroundColorAttributeName), whereas the rest of the new label simply uses the same text as before, however also contained in an NSAttributedTextString.
An example follows below using a function to repeatedly update your existing labels to attributed strings prefixed with a red *:
class ViewController: UIViewController {
#IBOutlet weak var myFirstLabel: UILabel!
#IBOutlet weak var mySecondLabel: UILabel!
let myPrefixCharacter = "*"
let myPrefixColor = UIColor.redColor()
// ...
override func viewDidLoad() {
super.viewDidLoad()
// ...
/* update labels to attributed strings */
updateLabelToAttributedString(myFirstLabel)
updateLabelToAttributedString(mySecondLabel)
// ...
}
func updateLabelToAttributedString(label: UILabel) {
/* original label text as NSAttributedString, prefixed with " " */
let attr = [ NSForegroundColorAttributeName: myPrefixColor ]
let myNewLabelText = NSMutableAttributedString(string: myPrefixCharacter, attributes: attr)
let myOrigLabelText = NSAttributedString(string: " " + (label.text ?? ""))
/* set new label text as attributed string */
myNewLabelText.appendAttributedString(myOrigLabelText)
label.attributedText = myNewLabelText
}
// ...
}
Swift 4
(Note: notation for attributed string key is changed in swift 4)
Here is an extension for NSMutableAttributedString, that add/set color on string/text.
extension NSMutableAttributedString {
func setColor(color: UIColor, forText stringValue: String) {
let range: NSRange = self.mutableString.range(of: stringValue, options: .caseInsensitive)
self.addAttribute(NSAttributedStringKey.foregroundColor, value: color, range: range)
}
}
Now, try above extension with UILabel and see result
let label = UILabel()
label.frame = CGRect(x: 40, y: 100, width: 280, height: 200)
let red = "red"
let blue = "blue"
let green = "green"
let stringValue = "\(red)\n\(blue)\n&\n\(green)"
label.textColor = UIColor.lightGray
label.numberOfLines = 0
let attributedString: NSMutableAttributedString = NSMutableAttributedString(string: stringValue)
attributedString.setColor(color: UIColor.red, forText: red) // or use direct value for text "red"
attributedString.setColor(color: UIColor.blue, forText: blue) // or use direct value for text "blue"
attributedString.setColor(color: UIColor.green, forText: green) // or use direct value for text "green"
label.font = UIFont.systemFont(ofSize: 26)
label.attributedText = attributedString
self.view.addSubview(label)
func updateAttributedStringWithCharacter(title : String, uilabel: UILabel) {
let text = title + "*"
let range = (text as NSString).range(of: "*")
let attributedString = NSMutableAttributedString(string:text)
attributedString.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.red , range: range)
uilabel.attributedText = attributedString }
I know this is an old post, but i want to share my approach (which is based on the answer from dfri) i just made it a function for convenience.
func lastCharOnColor(label: UILabel, color: UIColor, length: Int) {
//First we get the text.
let string = label.text
//Get number of characters on string and based on that get last character index.
let characterCounter = string?.characters.count
let lastCharacterIndex = characterCounter!-1
//Set Range
let range = NSRange(location: lastCharacterIndex, length: length)
let attributedString = NSMutableAttributedString(string: string!, attributes: nil)
//Set label
attributedString.addAttribute(NSForegroundColorAttributeName, value: color, range: range)
label.attributedText = attributedString
}
I use this function to just set the last character from a label to a certain color like this:
lastCharOnColor(label: self.labelname, color: UIColor.red, length: 1)
Hope this helps anyone.
Here is an extension for simply making mandatory labels by appending a red *
Swift 5
extension UILabel {
func makeTextMandatory() {
let text = self.text ?? "" + " *"
let range = (text as NSString).range(of: " *")
let attributedString = NSMutableAttributedString(string:text)
attributedString.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.red , range: range)
self.attributedText = attributedString
}
}
Usage :
dobLabel.makeTextMandatory()
I am resizing a UITextView for an attributed string that I create based on a response from my API that can have up to three different attributes; general terms, shipping terms, and return terms. If one of the attributes is missing, I don't show that attribute in the final text. On the iPhone 6, everything shows up exactly the way it should, but on the iPhone 5/5s, the last few lines of text get cutt off. The textView is resizing properly, but it doesn't show all of the text up until the last few lines. I wish I could show photos, but apparently I don't have enough rep to do so. Every other similar issue that I've searched isn't quite the same, and none of those solutions seem to fix the problem. Here is my code, in Swift, for creating the attributed string and resizing the textView. Any help would be greatly appreciated.
This is the code for formatting the attributed string
var termStrings = String()
if (!generalString.isEqualToString("")) {
termStrings += "GENERAL:\n\(generalString)\n\n"
}
if (!shippingString.isEqualToString("")) {
termStrings += "SHIPPING:\n\(shippingString)\n\n"
}
if (!returnString.isEqualToString("")) {
termStrings += "RETURNS:\n\(returnString)"
}
var newNSString = termStrings as NSString
var attTermString = NSMutableAttributedString(string: termStrings as String)
let boldAttribute = [NSForegroundColorAttributeName: UIColor.blackColor(), NSFontAttributeName: UIFont.boldSystemFontOfSize(14)]
let subtitleAttribute = [NSForegroundColorAttributeName: UIColor.blackColor(), NSFontAttributeName: UIFont.systemFontOfSize(12)]
let underlineAttribute = [NSUnderlineStyleAttributeName: NSUnderlineStyle.StyleSingle]
var paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 5.0
if (!generalString.isEqualToString("")) {
attTermString.addAttributes(boldAttribute, range: newNSString.rangeOfString("GENERAL:"))
attTermString.addAttribute(NSUnderlineStyleAttributeName, value: NSUnderlineStyle.StyleSingle.rawValue, range: newNSString.rangeOfString("GENERAL:"))
attTermString.addAttribute(NSParagraphStyleAttributeName, value: paragraphStyle, range: newNSString.rangeOfString("GENERAL:\n"))
attTermString.addAttributes(subtitleAttribute, range: newNSString.rangeOfString("\(generalString)"))
}
if (!shippingString.isEqualToString("")) {
attTermString.addAttributes(boldAttribute, range: newNSString.rangeOfString("SHIPPING:"))
attTermString.addAttribute(NSUnderlineStyleAttributeName, value: NSUnderlineStyle.StyleSingle.rawValue, range: newNSString.rangeOfString("SHIPPING:"))
attTermString.addAttribute(NSParagraphStyleAttributeName, value: paragraphStyle, range: newNSString.rangeOfString("SHIPPING:\n"))
attTermString.addAttributes(subtitleAttribute, range: newNSString.rangeOfString("\(shippingString)"))
}
if (!returnString.isEqualToString("")) {
attTermString.addAttributes(boldAttribute, range: newNSString.rangeOfString("RETURNS:"))
attTermString.addAttribute(NSUnderlineStyleAttributeName, value: NSUnderlineStyle.StyleSingle.rawValue, range: newNSString.rangeOfString("RETURNS:"))
attTermString.addAttribute(NSParagraphStyleAttributeName, value: paragraphStyle, range: newNSString.rangeOfString("RETURNS:\n"))
attTermString.addAttributes(subtitleAttribute, range: newNSString.rangeOfString("\(returnString)"))
}
changeTextViewHeight(attTermString, width: textView.frame.size.width)
This is my code for resizing the textView
func changeTextViewHeight(attributedString: NSMutableAttributedString, width: CGFloat) {
textView.attributedText = attributedString
var newSize: CGSize = textView.sizeThatFits(CGSizeMake(width, CGFloat(FLT_MAX)))
var newFrame: CGRect = textView.frame
newFrame.size = CGSizeMake(CGFloat(fmaxf((Float)(newSize.width), (Float)(width))), newSize.height)
textView.frame = newFrame
}
Steps to resolve (This will set the UITextView scroll to the top in 5/5s)
Disable the UITextView scroll view
set scrollRectToVisible Enable
UITextView scroll
Swift 3:
self.yourTextView.isScrollEnabled = false
let rect:CGRect = CGRect(x: 0, y: 0, width: 1, height: 1)
self.yourTextView.scrollRectToVisible(rect, animated: false)
self.yourTextView.isScrollEnabled = true
This Worked for me.