Word Wrapping Bug in NSTextField?

I am having an issue with text not wrapping correctly if there is a single quote, or macOS ASCII Extended Character #213 (shift+opt.+]) in a string.

Apple does not escape the media item title string when it is retrieved through the iTunesLibrary framework, which is where I ran into this problem.

As you can see in the example below, the first string is exactly how it comes from the iTunesLibrary using the framework API call. The second string is with the single quote is escaped, the third string is if I use macOS Extended ASCII Character code 213, and the fourth string is if I use a tilde. The tilde is not the right character to use in this situation, but it is the only one that correctly wraps the text in the cell.

I worked on this for 6-8 hours to figure it out before posting this on StackOverflow.

This is the result I get:

Anyone else get the same result running this?

Do I need to report this as a bug?

Here's my code example:

ViewController.swift

import Cocoa

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.frame.size = NSSize(width: 616, height: 184)
        // Strings
        let strings: Array<String> = ["I Keep Forgettin' (Every Time You're Near)","I Keep Forgettin\' (Every Time You're Near)","I Keep Forgettin’ (Every Time You're Near)","I Keep Forgettin` (Every Time You're Near)"]
        // Formatting
        let foreground = NSColor.purple.cgColor
        let paragraphStyle = NSMutableParagraphStyle()
        paragraphStyle.alignment = .center
        paragraphStyle.lineBreakMode = .byWordWrapping
        paragraphStyle.tabStops = .none
        paragraphStyle.baseWritingDirection = .leftToRight
        guard let font = NSFont(name: "Helvetica", size: 28.0) else { return }
        // Labels
        var labels: Array<NSTextField> = [NSTextField]()
        for i in 0..<strings.count {
            let label = NSTextField(frame: NSRect(x: 20+(i*144), y: Int(self.view.frame.minY)+20, width: 144, height: 144))
            label.cell = VerticallyCenteredTextFieldCell()
            label.wantsLayer = true
            label.layer?.borderColor = NSColor.purple.cgColor
            label.layer?.borderWidth = 0.5
            label.layer?.backgroundColor = NSColor.lightGray.cgColor
            label.alphaValue = 1
            let fontSize = bestFontSize(attributedString: NSAttributedString(string: strings[i], attributes: [.font: font, .paragraphStyle: paragraphStyle]), size: CGSize(width: 136, height: 136))
            label.attributedStringValue = NSAttributedString(string: strings[i], attributes: [.font: font.withSize(fontSize), .foregroundColor: foreground, .paragraphStyle: paragraphStyle])
            labels.append(label)
            self.view.addSubview(labels[i])
        }
    }

    override var representedObject: Any? {
        didSet {
        // Update the view, if already loaded.
        }
    }

    func bestFontSize(attributedString: NSAttributedString, size: CGSize) -> CGFloat {
        // Create a property to hold the font and size
        var font: NSFont?
        // Get the font information from the string attibutes
        attributedString.enumerateAttribute(.font, in: NSRange(0..<attributedString.length)) { value, range, stop in
            if let attrFont = value as? NSFont {
                font = attrFont
            }
        }
        if font == nil {
            return 0
        }
        // Get any paragraph styling attributes
        var paragraphStyle: NSMutableParagraphStyle?
        attributedString.enumerateAttribute(.paragraphStyle, in: NSMakeRange(0, attributedString.length)) { value, range, stop in
            if let style = value as? NSMutableParagraphStyle {
                paragraphStyle = style
            }
        }
        if paragraphStyle == nil {
            return 0
        }
        // Create a sorted list of words from the string in descending order of length (chars) of the word
        let fragment = attributedString.string.split(separator: " ").sorted() { $0.count > $1.count }
        // Create a bounding box size that will be used to check the width of the largest word in the string
        var width = String(fragment[0]).boundingRect(with: CGSize(width: .greatestFiniteMagnitude, height: size.height), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [.font: font!, .paragraphStyle: paragraphStyle!], context: nil).width.rounded(.up)
        // Create a bounding box size that will be used to check the height of the string
        var height = attributedString.string.boundingRect(with: CGSize(width: size.width, height: .greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [.font: font!, .paragraphStyle: paragraphStyle!], context: nil).height.rounded(.up)
        while height >= size.height || width >= size.width {
            guard let pointSize = font?.pointSize else {
                return 0
            }
            font = font?.withSize(pointSize-0.25)
            width = String(fragment[0]).boundingRect(with: CGSize(width: .greatestFiniteMagnitude, height: size.height), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [.font: font!, .paragraphStyle: paragraphStyle!], context: nil).width.rounded(.up)
            height = attributedString.string.boundingRect(with: CGSize(width: size.width, height: .greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [.font: font!, .paragraphStyle: paragraphStyle!], context: nil).height.rounded(.up)
        }
        return font!.pointSize
    }
}

VerticallyCenteredTextFieldCell.swift

import Cocoa

class VerticallyCenteredTextFieldCell: NSTextFieldCell {

    // https://stackoverflow.com/questions/11775128/set-text-vertical-center-in-nstextfield/33788973 - Sayanti Mondal
    func adjustedFrame(toVerticallyCenterText rect: NSRect) -> NSRect {
        // super would normally draw from the top of the cell
        var titleRect = super.titleRect(forBounds: rect)
        let minimumHeight = self.cellSize(forBounds: rect).height
        titleRect.origin.y += (titleRect.height - minimumHeight) / 2
        titleRect.size.height = minimumHeight
        return titleRect
    }
    
    override func drawInterior(withFrame cellFrame: NSRect, in controlView: NSView) {
        super.drawInterior(withFrame: adjustedFrame(toVerticallyCenterText: cellFrame), in: controlView)
    }
}

Replies

A bug report would be appreciated! Please post the feedback number here for reference.

Done. FB11785400

  • Thanks for the bug report. I'm following up internally to see if I can get any insight into what's going on here.

Add a Comment