NSTextFieldにadjustsFontSizeToFitWidthがないから自作する
UIKitのUILabelにはadjustsFontSizeToFitWidthという、ラベルの内容が1行に収まらない際、横幅に合わせてフォントサイズを調整してくれる機能があるのですが、macOSのアプリ開発でラベルとして使用するNSTextFieldにはこれに相当する機能がありません。
ならば自作するしかないよね。
文字列のサイズを求める
ラベルを自作するにあたって、任意の文字列を任意のフォントで描画するのに必要なサイズを求める必要があります。
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電卓とかで利用しています。
コメント