Text Wrapping not happening as it should in NSTextField Attributed String

153 views Asked by At

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.

As you can see in the example below, the first string is exactly how it come from the iTunesLibrary using the framework API call. The second string is is 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've been working on this for the past 6-8 hours to figure it out and I'm just throwing it out there to see if someone can help me.

ViewController.swift

import Cocoa

class ViewController: NSViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.frame.size = NSSize(width: 616, height: 184)
        // Strings
        let string1 = "I Keep Forgettin' (Every Time You're Near)"
        let string2 = "I Keep Forgettin\' (Every Time You're Near)"
        let string3 = "I Keep Forgettin’ (Every Time You're Near)"
        let string4 = "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
        let label1 = NSTextField(frame: NSRect(x: 20, y: self.view.frame.minY+20, width: 144, height: 144))
        label1.cell = VerticallyCenteredTextFieldCell()
        label1.wantsLayer = true
        label1.layer?.borderColor = NSColor.purple.cgColor
        label1.layer?.borderWidth = 0.5
        label1.layer?.backgroundColor = NSColor.lightGray.cgColor
        label1.alphaValue = 1
        var fontSize = bestFontSize(attributedString: NSAttributedString(string: string1, attributes: [.font: font, .paragraphStyle: paragraphStyle]), size: CGSize(width: 136, height: 136))
        label1.attributedStringValue = NSAttributedString(string: string1, attributes: [.font: font.withSize(fontSize), .foregroundColor: foreground, .paragraphStyle: paragraphStyle])
        self.view.addSubview(label1)

        let label2 = NSTextField(frame: NSRect(x: 164, y: self.view.frame.minY+20, width: 144, height: 144))
        label2.cell = VerticallyCenteredTextFieldCell()
        label2.wantsLayer = true
        label2.layer?.borderColor = NSColor.purple.cgColor
        label2.layer?.borderWidth = 0.5
        label2.layer?.backgroundColor = NSColor.lightGray.cgColor
        label2.alphaValue = 1
        fontSize = bestFontSize(attributedString: NSAttributedString(string: string2, attributes: [.font: font, .paragraphStyle: paragraphStyle]), size: CGSize(width: 136, height: 136))
        label2.attributedStringValue = NSAttributedString(string: string2, attributes: [.font: font.withSize(fontSize), .foregroundColor: foreground, .paragraphStyle: paragraphStyle])
        self.view.addSubview(label2)

        let label3 = NSTextField(frame: NSRect(x: 308, y: self.view.frame.minY+20, width: 144, height: 144))
        label3.cell = VerticallyCenteredTextFieldCell()
        label3.wantsLayer = true
        label3.layer?.borderColor = NSColor.purple.cgColor
        label3.layer?.borderWidth = 0.5
        label3.layer?.backgroundColor = NSColor.lightGray.cgColor
        label3.alphaValue = 1
        fontSize = bestFontSize(attributedString: NSAttributedString(string: string3, attributes: [.font: font, .paragraphStyle: paragraphStyle]), size: CGSize(width: 136, height: 136))
        label3.attributedStringValue = NSAttributedString(string: string3, attributes: [.font: font.withSize(fontSize), .foregroundColor: foreground, .paragraphStyle: paragraphStyle])
        self.view.addSubview(label3)

        let label4 = NSTextField(frame: NSRect(x: 452, y: self.view.frame.minY+20, width: 144, height: 144))
        label4.cell = VerticallyCenteredTextFieldCell()
        label4.wantsLayer = true
        label4.layer?.borderColor = NSColor.purple.cgColor
        label4.layer?.borderWidth = 0.5
        label4.layer?.backgroundColor = NSColor.lightGray.cgColor
        label4.alphaValue = 1
        fontSize = bestFontSize(attributedString: NSAttributedString(string: string4, attributes: [.font: font, .paragraphStyle: paragraphStyle]), size: CGSize(width: 136, height: 136))
        label4.attributedStringValue = NSAttributedString(string: string4, attributes: [.font: font.withSize(fontSize), .foregroundColor: foreground, .paragraphStyle: paragraphStyle])
        self.view.addSubview(label4)
    }

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

This is the result I get: enter image description here Anyone else get the same result running this?

0

There are 0 answers