😽

[iOS]BoldなFontをItalicにするにはどうすればいいか

2021/02/25に公開約4,000字

はじめに

NSMutableAttributedStringで既に装飾が施されているテキストの特定範囲へ,BoldやItaclicの装飾を施すにはどうすればいいでしょうか?

stackoverflowにて検索すると,次のような回答があります.どうやらfontDescriptorを使えば,fontSizeなどは維持したままに,BoldやItalicのFontへ変更できるようです.

extension UIFont {

    func withTraits(_ traits: UIFontDescriptorSymbolicTraits) -> UIFont {

        // create a new font descriptor with the given traits
        guard let fd = fontDescriptor.withSymbolicTraits(traits) else {
            // the given traits couldn't be applied, return self
            return self
        }

        // return a new font with the created font descriptor
        return UIFont(descriptor: fd, size: pointSize)
    }

    func italics() -> UIFont {
        return withTraits(.traitItalic)
    }

    func bold() -> UIFont {
        return withTraits(.traitBold)
    }

    func boldItalics() -> UIFont {
        return withTraits([ .traitBold, .traitItalic ])
    }
}

[1] How to apply bold and italics to an NSMutableAttributedString range? https://stackoverflow.com/questions/34499735/how-to-apply-bold-and-italics-to-an-nsmutableattributedstring-range

しかし,この実装をそのまま用いると問題となるケースがあります.それは,bold()の呼び出しの後に,italic()を呼ぶ,あるいはその逆順でそれぞれを呼ぶケースです.この場合,あとから呼んだメソッドによってFontがオーバーライドされます.予め両方のスタイルが適応されることが分かっていれば始めからboldItalics()を呼べば済みますが,バックエンドから得た情報を基に描画する時やユーザの入力によって変わる時など,予め分からないケースも多々あります.

👆 boldでオーバーライドされている例

一方で,次のようにif分岐を用いることで目的の結果を得る事ができます.しかしこれは,シンプルな実装とは言えません.Bold],Italicに続いてmonoSpacingなFontが追加されたときは,さらに組み合わせの数が増えてしまいます.

let v = UILabel(frame: CGRect(origin: .zero, size: .zero))

if styles.contains(.bold) && styles.contains(.italic) {
    v.font = v.font.boldItalics()
} else styles.contains(.bold) {
    v.font = v.font.bold()
} else {
    v.font = v.font.italic()
}

そこで,次の条件を満たすような実装を考えてみます.

  • メソッドの呼び出し順序によって結果が変わらない
  • if分岐などを用いずに簡潔に実装できる

SymbolicTraitsModiferの実装

まず,メソッドの呼び出し順序によって結果が変わらないようにするためには,UIFontの生成を遅延させる必要があります.[1]のboldItalics()をみても分かる通り,withSymbolicTraits()に対して適応させたいtraitsを全て渡してあげると,良い感じに意図したUIFontを生成できるUIFontDescriptorを作れます.UIFontを開発者の意図したタイミングで生成するには,どんなtraitsが選択されているかを保持しておく必要があるので,そのためのクラスを作ります.

以下がそのクラスです.従来とは違いbold()italic()の呼び出し時にはtraitsの記録だけにとどめています.そして,build()を呼び出したタイミングでUIFontの生成を行えます.

final class SymbolicTraitsModifer {
    private let font: UIFont
    private var traits: UIFontDescriptor.SymbolicTraits = []

    init(font: UIFont) {
        self.font = font
    }

    func bold() -> SymbolicTraitsModifer {
        traits.insert(.traitBold)
        return self
    }

    func italic() -> SymbolicTraitsModifer {
        traits.insert(.traitItalic)
        return self
    }

    func build() -> UIFont {
        if let descriptor = font.fontDescriptor.withSymbolicTraits(traits) {
            return UIFont(descriptor: descriptor, size: font.pointSize)
        } else {
            return font
        }
    }
}

利用例は次のとおりです.ちゃんとboldとitalicの装飾が付いています.

この利用例ではSymbolicTraitsModiferを簡単に参照するために,次のような拡張をしています.

extension UIFont {
    var stm: SymbolicTraitsModifer {
        SymbolicTraitsModifer(font: self)
    }
}

しかし,このままではまだうまくいかないケースがあります.それは,もともとBoldだったfontに対してイタリックにしようとした場合です.コードを書くと,ItalicにオーバーライドされてBoldが消えてしまっていることが分かります.既にBoldが適応されていてもwithSymbolicTraits().traitBoldを与える必要があるようです.

そこで,SymbolicTraitsModiferのinitializerを次のように変更します.これで,既存のsymbolicTraitsもbuild時に一緒に渡してあげられます.

init(font: UIFont) {
    self.font = font
    traits = font.fontDescriptor.symbolicTraits
}

NSMutableAttributedStringへの適応

NSMutableAttributedStringを使えば簡単に部分適用できます.データ構造をこね回して,適応範囲とbold,italic,およびその両方との対応関係を作る時はSymbolicTraitsModiferの威力を発揮しそうです.

おわりに

NSMutableAttributedStringの特定の範囲へ簡単にBold,Italic,および両方の装飾を施す方法を紹介しました.今回紹介したのはBoldとItalicだけですが,その他にもいろいろできるのでぜひためしてみてください.

それでは.

Discussion

ログインするとコメントできます