5
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

iOS でサイズやスタイルを維持したままフォントを一括変換

Last updated at Posted at 2020-02-14

目標

  • フォント (Font Family) を一括で変換する。
  • 指定済みの Size, Weight, Italic を維持する。
  • Weight はフォントによって数が異なるため、指定済みに最も近い Weight を利用する。
  • どの Font Family にも適用できるよう汎用性の高いコードにする。

完成型

解説はどうでもいいのでコードの全体像を見たいという方はどうぞ

クリックして開く
extension UIFont {

    convenience init?(familyName: String, weight: UIFont.Weight, isItalic: Bool = false, size: CGFloat) {
        let font = UIFont
            .fontNames(forFamilyName: familyName)
            .compactMap({ UIFont(name: $0, size: size) })
            .filter({ $0.isItalic == isItalic })
            .min(by: { abs($0.weight.rawValue - weight.rawValue) < abs($1.weight.rawValue) })
        self.init(name: font?.fontName ?? "", size: size)
    }

    private var traits: [UIFontDescriptor.TraitKey: Any] {
        return fontDescriptor.object(forKey: .traits) as? [UIFontDescriptor.TraitKey: Any] ?? [:]
    }
    
    var weight: UIFont.Weight {
        guard let weight = traits[.weight] as? NSNumber else { return .regular }
        return UIFont.Weight(rawValue: CGFloat(truncating: weight))
    }
    
    var isItalic: Bool {
        return traits[.slant] as? NSNumber != 0
    }
}

extension UILabel {

    @objc var fontFamily: String {
        set {
            guard let font = UIFont(familyName: newValue, weight: self.font.weight, isItalic: self.font.isItalic, size: self.font.pointSize) else { return }
            self.font = font
        }
        get {
            return self.font.familyName
        }
    }
}

// 使用時
UILabel.appearance().fontFamily = "Avenir"

最初に

UILabel などは、UIAppearance を用いることで、特定のプロパティを一括して設定できます。

UILabel.apperance().font = UIFont(name: "Gills Sans", size: 14)

これに加え、Weight (文字の太さ) なども考慮した上でフォントファミリーを指定する方法が、下記で良く解説されています。
https://qiita.com/yfujiki/items/7de9421e63dfbfbcc7d4

これを参考にさせていただきつつ、
Italic や、多数の Weight などへの考慮もするため、下記で解説するようなコードで実装しました。

フォント周りの理解

コードの前に、フォント周りについて少し説明します。理解してる人は読み飛ばしてください。

Family Names と Font Name

例で2種類だしてみました。違いを理解しておいてください。

Family Name Font Name
Helvetica Neue HelveticaNeue-Thin
HelveticaNeue-ThinItalic
HelveticaNeue
HelveticaNeue-Italic
etc...
Hiragino Sans HiraginoSans-W3
HiraginoSans-W6
HiraginoSans-W7

ここで大事なのは

  • 各 Family Name に全ての Weight / Italic があるわけではない

    もともと指定していた Weight に最も近い Weight を取得すべき
  • Font Name の命名規則に統一性は無い 1

    名称 (Light, Thin, など) をもとに Weight を判定すべきでない

Family と Name の取得 / 確認方法

A. Mac に入っている Font Book.app でみる

「PostScript名」が Font Name、「ファミリー」が Font Family です。2

B. コードで確認する

Apple 公式記事 からコピペしました。

for family in UIFont.familyNames.sorted() {
    let names = UIFont.fontNames(forFamilyName: family)
    print("Family: \(family) Font names: \(names)")
}

UIFont のイニシャライザ

UIFont.init?(name fontName: String, size fontSize: CGFloat) を利用します。3
つまり、Font Family ではなく Font Name を使用することになります。

解説

1. Weight と Italic を取得する

UIFontDescriptor 経由で取得します。
UIFontDescriptor.TraitKey.slant で傾き具合を取得できるので、傾きが 0 でない場合に Italic とします。

extension UIFont {

    private var traits: [UIFontDescriptor.TraitKey: Any] {
        return fontDescriptor.object(forKey: .traits) as? [UIFontDescriptor.TraitKey: Any] ?? [:]
    }
    
    var weight: UIFont.Weight {
        guard let weight = traits[.weight] as? NSNumber else { return .regular }
        return UIFont.Weight(rawValue: CGFloat(truncating: weight))
    }
    
    var isItalic: Bool {
        return traits[.slant] as? NSNumber != 0
    }
}

2. Font Family + Weight + Italic + Size でイニシャライズ

これらを踏まえ、Font Family・Weight・Italic・Size を引数としたイニシャライザを用意します。

  1. Family Name をもとに Font Name を取得
  2. Font Name をもとに UIFont を取得 (ここの Size はなんでもいい)
  3. Italic か否かでフィルタリング
  4. 引数で指定した Weight に最も近い Weight のものを取得
  5. イニシャライズ 4
extension UIFont {

    convenience init?(familyName: String, weight: UIFont.Weight, isItalic: Bool = false, size: CGFloat) {
        let font = UIFont
            .fontNames(forFamilyName: familyName)                                             // 1
            .compactMap({ UIFont(name: $0, size: size) })                                     // 2
            .filter({ $0.isItalic == isItalic })                                              // 3
            .min(by: { abs($0.weight.rawValue - weight.rawValue) < abs($1.weight.rawValue) }) // 4
        self.init(name: font?.fontName ?? "", size: size)                                     // 5
    }
}

3. UILabel で Font Family を指定できるように

これで終わりです。
用意した Weight / Italic get-only property と initializer を使用し、Font Family をもとにフォントを変更できるようにします。
存在しない Faimly Name を指定すると無視されます。
@objc をつけることで UIAppearance を使用できます。

extension UILabel {
    
    /// Set value that can be obtained from `UIFont.familyNames`.
    /// Can use UIAppearance because of using `@objc`.
    @objc var fontFamily: String {
        set {
            guard let font = UIFont(familyName: newValue, weight: self.font.weight, isItalic: self.font.isItalic, size: self.font.pointSize) else { return }
            self.font = font
        }
        get {
            return self.font.familyName
        }
    }
}
使用する時
UILabel.appearance().fontFamily = "Avenir"

環境 / 参考

Swift iOS
5 13
  1. 大体のフォントは <FamilyName>-<Weight><Italic> のような感じですが、例で挙げたような Hiragino Sans パターンなどもありえます。

  2. このアプリ内で表示されているフォントはPC内のものなので、開発中のアプリ内で全て使用できるわけではありません。逆に載っていないものも使用できるものがあります。

  3. init(descriptor: UIFontDescriptor, size pointSize: CGFloat) というのもあります

  4. convenience イニシャライザを使用しているので self.init していますが、 class関数でも良ければ、そのまま font 変数を返すだけで良いですね。お好みでどうぞ。

5
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?