Mick F
Mick F

Reputation: 7439

Emoji support for NSAttributedString attributes (kerning/paragraph style)

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:

/Users/mick/Desktop/Capture d’écran 2018-03-30 Γ  16.45.55.png

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.

Upvotes: 5

Views: 1030

Answers (1)

Code Different
Code Different

Reputation: 93191

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.

Upvotes: 9

Related Questions