ひtoりgoと

その数字、ガタガタしてない? - AppKit で数字を等幅にする

macOS 開発

OS X 10.9-10.11 で連続してシステムフォントが変更された。

  • 10.9 で .Lucida Grande UI(ほとんど変化なし)
  • 10.10 で Helvetica Neue
  • 10.11 で San Francisco

このうち 10.11 で採用された San Francisco が現在も使われている。使い方に合わせてあらゆる部分がダイナミックに変化することによって使用シーンの幅を拡大し、今では Apple 製品全体でおなじみ(スクリーン上のみならず製品のロゴやキーボードの刻印にも使われるようになった)の存在である。

どこで使っても読みやすくスタイリッシュで万能感のあるこのフォントだけど、UI で使うときに一つ気をつけておきたいことがある。数字部分がプロポーショナルになったことだ。

数字は変化する頻度が高いのでこのまま使うと値が増減するたびに幅が変化して表示が左右にずれてしまうことになる。これに対処するためには

...という話は iOS(UIKit)の世界では十分に知られているはず。今回は情報の少ない Mac App 開発のための記事。AppKit でも UIKit とほぼ同様、 NSFontmonospacedDigitSystemFont(ofSize:weight:) というクラスメソッドが追加されている。

Interface Builder 上でもフォントパネルのアクションメニューから "Typography..." を選択して表示されるパネルの中に "Number Spacing" という項目があるのでこれを "Monospaced Numbers" に変更すれば再現可能...なのだが、なぜかここでの指定は IB 上の表示に反映されるもののアプリケーションを動かすとリセットされてしまう。(Xcode 9.0)

Interface Builder
実際

フォントを変換するメソッド

それでも簡単な変換メソッドを用意すればすむ話である:

extension NSFont
{
    @available(macOS 10.11, *)
    public var monospacedDigit: NSFont?
    {
        let additionalFeatureSettings: [[NSFontDescriptor.FeatureKey: AnyObject]] = [
            [
                .typeIdentifier: (kNumberSpacingType as AnyObject),
                .selectorIdentifier: (kMonospacedNumbersSelector as AnyObject)
            ]
        ]
        let fontDescriptor = self.fontDescriptor.addingAttributes([.featureSettings: additionalFeatureSettings])
        return NSFont(descriptor: fontDescriptor, size: self.pointSize)
    }
}
extension NSControl
{
    public func setFontToMonospacedDigit()
    {
        if #available(macOS 10.11, *) {
            if let monospacedDigitFont = self.font?.monospacedDigit {
                self.font = monospacedDigitFont
            }
        }
    }
}

NSControl を拡張したのでボタンでもテキストフィールドでもラベルでも、同じ方法で数字を等幅化できる。これは共通の font プロパティを持たない UIControl にはできない技だ。(← AppKit が叩かれすぎて小さいことでもいちいち UIKit に優越感を持ちたくなる歪んだ Mac App 開発者)

[button, textField, label].forEach { $0.setFontToMonospacedDigit() }

Playground での実験

実際に違いを確認してみよう。

//Prepare controls in a stack view:
let controls = [
    NSButton(title: "000000", target: nil, action: nil),
    NSButton(title: "111111", target: nil, action: nil),
    NSTextField(string: "000000"),
    NSTextField(string: "111111")
]
controls.forEach { $0.addConstraint($0.widthAnchor.constraint(equalToConstant: 70)) }
//controls.forEach { $0.font = NSFont.boldSystemFont(ofSize: 13) }
let stackView = NSStackView(views: controls); do {
    stackView.orientation = .vertical
    stackView.edgeInsets = NSEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
}

//Before:
stackView.layoutSubtreeIfNeeded()
stackView //Result 1

//After:
controls.forEach { $0.setFontToMonospacedDigit() }
stackView.layoutSubtreeIfNeeded()
stackView //Result 2
Result 1
Result 2

数字の中でも特に "1" の幅が狭くて違いが顕著に現れる。9 行目のコメントを外せばボールドでも確認可能。

---

オブジェクトをドラッグしたときや矢印キーを使って値を増減したりするたびに数字がガタガタ揺れたりしていると見苦しいので、こういう部分にも気を使いましょう。

Share

リンクも共有もお気軽に。記事を書くモチベーションの向上に役立てます。

© 2005-2022 zumuya