user187676
user187676

Reputation:

NSAttributedString boundingRect returns wrong height

I calculate the height of an NSAttributedString like this.

let maxSize = NSSize(width: w, height: CGFloat.greatestFiniteMagnitude)
let rect = boundingRect(with: maxSize, options: [.usesFontLeading, .usesLineFragmentOrigin])
let height = rect.integral.size.height

I tried about every "hack" that was mentioned on SO, yet the string height gets more inaccurate the smaller the width gets (the calculated height is larger than the actual height).

According to other posts the following things cause problems with size calculation:

I have found that none of these suggestions make any difference. The attributed string is a concatenation of lines, each having a foregroundColor and backgroundColor. The string also has the following paragraph style.

 let pstyle = NSMutableParagraphStyle()
 pstyle.lineSpacing = 6
 pstyle.lineBreakMode = .byWordWrapping

and a userFixedPitchFont of size 11.

Why does the height error get larger the smaller the width is?

PS: I imagine it has something to do with lineBreak since the error gets larger if more lines are word wrapped.

Upvotes: 2

Views: 3419

Answers (1)

user187676
user187676

Reputation:

I found that this solution works. Note that if you don't set the lineFragmentPadding to 0, it produces the same (wrong) results as boundingRect.

extension NSAttributedString {
    func sizeFittingWidth(_ w: CGFloat) -> CGSize {
        let textStorage = NSTextStorage(attributedString: self)
        let size = CGSize(width: w, height: CGFloat.greatestFiniteMagnitude)
        let boundingRect = CGRect(origin: .zero, size: size)

        let textContainer = NSTextContainer(size: size)
        textContainer.lineFragmentPadding = 0

        let layoutManager = NSLayoutManager()
        layoutManager.addTextContainer(textContainer)

        textStorage.addLayoutManager(layoutManager)

        layoutManager.glyphRange(forBoundingRect: boundingRect, in: textContainer)

        let rect = layoutManager.usedRect(for: textContainer)

        return rect.integral.size
    }
}

Upvotes: 11

Related Questions