[Swift] typealias と extension で NSAttributedString を便利に使う

Swift

NSAttributedString を使うためには、多くの属性情報を Dictionary で渡す必要があります。typealias と extension を使うことで、すこしだけ簡単になる方法を説明します。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Big Sur 11.3 beta
  • Xcode 12.4

NSAttributedString

さまざまな属性を設定することにより、文字色や縁取り等を設定することができる String です。

以下のような表示ができるようになります。

NSAttributedString 例
NSAttributedString 例
MEMO
AppKit や UIKit では、NSTextField や UILabel を使って、表示することができますが、SwiftUI にはまだ用意されていません。
SwiftUI で NSAttributedString を表示できるビュー SDSAttributedText を作りました。→ こちら

使い方

さまざまな属性をセットした Dictionary を渡すことで、フォントやカラーを指定した NSAttributedString を作成することができます。

以下は、フォントには システムフォントをサイズ 60 で、色を赤、縁取りは幅2で黒 という指定の NSAttributedString を作るコードです。

NSAttributedString 設定例

var dic:[NSAttributedString.Key: Any] = [
        NSAttributedString.Key.font: NSFont.systemFont(ofSize: 60),
        NSAttributedString.Key.foregroundColor: NSColor.red,
        NSAttributedString.Key.strokeColor: NSColor.black,
        NSAttributedString.Key.strokeWidth: -2
    ]
let attrString = NSAttributedString(string: "Hello, world!", attributes: dic)

このように、Dictionary を使うことで、さまざまな属性を一度に設定として渡すことができるようになっています。

どのような設定ができるかは、Dictionary のキーとして設定される NSAttributedString.Key として 定義されています。

Apple のドキュメントは、こちら

非常に多くのキーが定義されています。さまざまな設定ができるのですが、間違いも発生しやすくなってしまいます。

間違いやすい点1:さまざまなキーに対して それぞれ型の違う Value が必要

さまざまな属性を設定できるように NSAttributedString.Key には、さまざまなキーが定義されています。
難しい点は、それぞれのキーには属性ごとに異なる情報を設定することが必要ということです。これは、値が異なるだけではなく、型が異なることもあるということです。

例えば、NSAttributedString.Key.strokeColor というキーに対しては、縁取りの色として 色(NSColor/UIColor) を設定することが必要ですが、
NSAttributedString.Key.strokeWidth には、縁取りの幅として 数値(NSNumber) を設定することが必要になります。

ただし、Dictionary は、[NSAttributedString.Key: Any] として定義されているので、以下のようなコードもコンパイルできてしまいます。

example

var dic:[NSAttributedString.Key: Any] = [
        NSAttributedString.Key.strokeColor: -3 // 色 (NSColor) が必要なのに、数字 (NSNumber) がセットされている!
    ]

異なる型の情報をセットしても、期待通りの動作はしません。

できれば、特定の型を Value として受けるように [NSAttributedString.Key: NSNumber] のような定義をしたいのですが、
さまざまな型を Value として受け入れる必要があるので、Dictionary の定義は、[NSAttributedString.Key: Any] と定義しないといけません。

ですので、Swift 言語的には、間違ったタイプのデータを Value として設定されてもチェックする方法がありません。

MEMO
Dictionary に設定する値を、Key と Value と書いています。
dic[key] = value

防ぐ方法としては、(このままでは) ”気をつける” 以外にありません。

間違いやすい点2:複数のキーが正しく設定されることが必要な属性がある

例えば、StrokeColor と StrokeWidth は、両方が設定されることで、属性として成立します。
一方だけを設定しても表示には反映されません。

Swift の Dictionary では、複数のキーを確実に設定させるというような制約は付与できません。

この点についても、防ぐ方法としては、”気をつける”以外にありません。

個人的にも何度かハマっていますので、気をつける以外の方法を作りました。

typealias と extension を使う

そのまま使い続けると”気をつける”を続ける必要がありますが、Swift のもつ別の機能を使って、間違いを防止することができます。

typealias SDSAttributedStringDic

typealias を使って、型に名前をつけることができます。NSAttributedString の属性設定に使用する Dictionary を SDSAttributedStringDic と名付けます。

SDSAttributedStringDic

typealias SDSAttributedStringDic = [NSAttributedString.Key: Any]

こうすることで、変数の意味と目的がその型からわかるようになります。

extension SDSAttributedStringDic(1)

Value の型をチェックするために、extension を使い Dictionary を直接操作しないようにします。

例えば、setFont というメソッドを extension で定義することで、NSAttributedString.Key.font というキーに対して、NSFont という型を持つ情報を間違いなく設定することができるようになります。

setFont

extension SDSAttributedStringDic {
    mutating func setFont(_ font: NSFont) {
        self.updateValue(font, forKey: .font)
    }
...
}

setFont メソッドは、引数として、NSFont を受け取り その後 キー"NSAttributedString.Key.font" に対応する Value として設定しています。
setFont は、メソッドとして受け取った段階で引数の型チェックが行われています。ですので、確実に NSFont 型の Value を設定することができます。

以下のように、入出力メソッドを揃えておくと、アクセスしやすくなります。

Font関連設定メソッド

extension SDSAttributedStringDic {
    // font
    var font: NSFont {
        if let font = self[.font] as? NSFont { return font }
        return NSFont.systemFont(ofSize: 24)     // 未設定時のデフォルト
    }
    mutating func setFont(_ font: NSFont) {
        self.updateValue(font, forKey: .font)
    }
    mutating func removeFont() {
        self.removeValue(forKey: .font)
    }
...
}
※ 未設定時にデフォルト値を返したいかどうかはケースバイケースかもしれません。

extension SDSAttributedStringDic(2)

複数の設定を行わないといけない情報については、複数の設定を強制するようなメソッドを作ることで間違いを防止します。

例えば、strokeColor は、strokeWidth と同時に設定するようなメソッドを用意することで、一方だけが設定されているという状態が作られないようになります。

strokeColor & Width 一括設定メソッド

    // strokeColor & Width
    mutating func setStrokeColorAndWidth(_ givenColor: NSColor? = nil,_ givenWidth: NSNumber? = nil) {
        let color = (givenColor != nil) ? givenColor! : self.strokeColor
        let width = givenWidth != nil ? givenWidth! : self.strokeWidth
        self.updateValue(color, forKey: .strokeColor)
        self.updateValue(width, forKey: .strokeWidth)
    }
    mutating func removeStrokeColorAndWidth() {
        self.removeValue(forKey: .strokeColor)
        self.removeValue(forKey: .strokeWidth)
    }

    var strokeColor: NSColor {
        if let color = self[.strokeColor] as? NSColor { return color }
        return NSColor.black
    }
    var strokeWidth: NSNumber {
        if let width = self[.strokeWidth] as? NSNumber { return width }
        return -5
    }

まとめ:typealias と extension で便利に NSAttributedString を使う

typealias と extension で便利に NSAttributedString を使う
  • NSAttributedString の属性は、Dictionary[NSAttributedString.Key:Any] を使って設定する
  • Dictionary に誤った情報をセットしないように extension 経由でアクセスすると間違いを防止できる
  • ※ SwiftUI には、NSAttributedString を表示するための要素は用意されていない

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

Swift おすすめ本

Swift を深く理解するには、以下の本がおすすめです。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です