Ideal Reality

興味の赴くままに

NSTextFieldにadjustsFontSizeToFitWidthがないから自作する

UIKitのUILabelにはadjustsFontSizeToFitWidthという、ラベルの内容が1行に収まらない際、横幅に合わせてフォントサイズを調整してくれる機能があるのですが、macOSのアプリ開発でラベルとして使用するNSTextFieldにはこれに相当する機能がありません。

ならば自作するしかないよね。

Contents
スポンサーリンク

文字列のサイズを求める

ラベルを自作するにあたって、任意の文字列を任意のフォントで描画するのに必要なサイズを求める必要があります。

func size(of str: String, for font: NSFont) -> NSSize {
    let attributes: [NSAttributedString.Key: Any] = [.font: font]
    let storage = NSTextStorage(string: str, attributes: attributes)
    let container = NSTextContainer(containerSize: NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude))
    let layoutManager = NSLayoutManager()
    layoutManager.addTextContainer(container)
    storage.addLayoutManager(layoutManager)
    container.lineFragmentPadding = 0
    layoutManager.glyphRange(for: container)
    return layoutManager.usedRect(for: container).size
}

NSLayoutManagerを利用します。ちなみに、containerの横幅を指定すれば文字列を折り返しした際の高さを求めたりできます。

文字列の描画

NSTextFieldを利用せず、直接NSViewのサブクラスを作成し、drawメソッド内で文字列の描画を行います。

override func draw(_ dirtyRect: NSRect) {
    
    // Background Drawing
    backgroundColor.setFill()
    dirtyRect.fill()
    
    let str = text ?? ""
    var fontSize = self.font.pointSize
    while let font = NSFont(descriptor: self.font.fontDescriptor, size: fontSize), size(of: str, for: font).width >= frame.width {
        fontSize -= 1
    }
    guard let font = NSFont(descriptor: self.font.fontDescriptor, size: fontSize) else { return }
    let paragraph = NSMutableParagraphStyle()
    paragraph.alignment = .right
    let attributes: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: textColor, .paragraphStyle: paragraph]
    let textSize = size(of: str, for: font)
    var textOrigin = NSPoint.zero
    switch textAlignment {
    case .center:
        textOrigin = NSPoint(x: (frame.width - textSize.width) / 2, y: (frame.height - textSize.height) / 2)
    case .right:
        textOrigin = NSPoint(x: frame.width - textSize.width, y: (frame.height - textSize.height) / 2)
    default:
        textOrigin = NSPoint(x: 0, y: (frame.height - textSize.height) / 2)
    }
    let textRect = NSRect(origin: textOrigin, size: textSize)
    str.draw(in: textRect, withAttributes: attributes)
}

adjustsFontSizeToFitWidthを使いたいだけだったので、文字列を1行表示するだけです。

指定されたフォントで描画するのに必要なサイズを導出し、それがviewの横幅よりも小さくなるまでwhile文でフォントサイズを小さくしていく形です。

スポンサーリンク

クラス全体

@IBDesignable class Label: NSView {
    
    @IBInspectable var text: String? {
        didSet { setNeedsDisplay(NSRect(origin: .zero, size: frame.size)) }
    }
    var font: NSFont = .systemFont(ofSize: 17) {
        didSet { setNeedsDisplay(NSRect(origin: .zero, size: frame.size)) }
    }
    @IBInspectable var textColor: NSColor = .labelColor {
        didSet { setNeedsDisplay(NSRect(origin: .zero, size: frame.size)) }
    }
    
    var textAlignment: NSTextAlignment = .natural {
        didSet { setNeedsDisplay(NSRect(origin: .zero, size: frame.size)) }
    }
    
    @IBInspectable var backgroundColor: NSColor = .clear {
        didSet { setNeedsDisplay(NSRect(origin: .zero, size: frame.size)) }
    }

    override func draw(_ dirtyRect: NSRect) {
        
        // Background Drawing
        backgroundColor.setFill()
        dirtyRect.fill()
        
        let str = text ?? ""
        var fontSize = self.font.pointSize
        while let font = NSFont(descriptor: self.font.fontDescriptor, size: fontSize), size(of: str, for: font).width >= frame.width {
            fontSize -= 1
        }
        guard let font = NSFont(descriptor: self.font.fontDescriptor, size: fontSize) else { return }
        let paragraph = NSMutableParagraphStyle()
        paragraph.alignment = .right
        let attributes: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: textColor, .paragraphStyle: paragraph]
        let textSize = size(of: str, for: font)
        var textOrigin = NSPoint.zero
        switch textAlignment {
        case .center:
            textOrigin = NSPoint(x: (frame.width - textSize.width) / 2, y: (frame.height - textSize.height) / 2)
        case .right:
            textOrigin = NSPoint(x: frame.width - textSize.width, y: (frame.height - textSize.height) / 2)
        default:
            textOrigin = NSPoint(x: 0, y: (frame.height - textSize.height) / 2)
        }
        let textRect = NSRect(origin: textOrigin, size: textSize)
        str.draw(in: textRect, withAttributes: attributes)
    }
    
    private func size(of str: String, for font: NSFont) -> NSSize {
        let attributes: [NSAttributedString.Key: Any] = [.font: font]
        let storage = NSTextStorage(string: str, attributes: attributes)
        let container = NSTextContainer(containerSize: NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude))
        let layoutManager = NSLayoutManager()
        layoutManager.addTextContainer(container)
        storage.addLayoutManager(layoutManager)
        container.lineFragmentPadding = 0
        layoutManager.glyphRange(for: container)
        return layoutManager.usedRect(for: container).size
    }
    
    func sizeToFit() {
        if let str = text {
            let size = self.size(of: str, for: font)
            frame.size = size
        } else {
            frame.size = .zero
        }
    }
}

サイズ導出や描画の他、UILabelと同様な使い方ができるようにインスタンス変数や、UILabelでsizeToFitをよく使うので定義してあったりします。

僕のアプリではRPN電卓とかで利用しています。

スポンサーリンク

コメント

投稿されたコメントはありません

名前

メールアドレス(任意)

コメント

関連する投稿

iOS13でViewController上部の隙間をなくす

[SwiftUI]List内のButtonやLinkのデザインをNavigationLinkっぽくする方法

[SwiftUI]Apple Watchで画面遷移から戻った際にアニメーションが崩壊するのを防ぐ

[SwiftUI]EnvironmentObjectをウィンドウごとに別々のインスタンスにしたい

SwiftのDecimal(string:)がどれだけ使えるか試してみた

‘configure(withApplicationID:)’ is deprecatedと言われた