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.
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
}
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.