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

# 目標

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

# 完成型

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

<details><summary>クリックして開く</summary><div>

```swift
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"
```
</div></details>


# 最初に

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

```swift
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<br>HelveticaNeue-ThinItalic<br>HelveticaNeue<br>HelveticaNeue-Italic<br>etc... |
| Hiragino Sans | HiraginoSans-W3<br>HiraginoSans-W6<br>HiraginoSans-W7 |


ここで大事なのは

- 各 Family Name に全ての Weight / Italic があるわけではない<br>
→ <font color="Red">もともと指定していた Weight に最も近い Weight を取得すべき</font>
- Font Name の命名規則に統一性は無い [^fontnamerule]<br>
→ <font color="Red">名称 (Light, Thin, など) をもとに Weight を判定すべきでない</font>

[^fontnamerule]: 大体のフォントは `<FamilyName>-<Weight><Italic>` のような感じですが、例で挙げたような Hiragino Sans パターンなどもありえます。

## Family と Name の取得 / 確認方法

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

「PostScript名」が Font Name、「ファミリー」が Font Family です。[^showfontapp]
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42826/5e0267a0-3202-b11c-6984-2363b5b84fcf.png">

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

### B. コードで確認する

[Apple 公式記事](https://developer.apple.com/documentation/uikit/text_display_and_fonts/adding_a_custom_font_to_your_app) からコピペしました。

```swift
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)`](https://developer.apple.com/documentation/uikit/uifont/1619041-init) を利用します。[^fontinit]
つまり、Font Family ではなく Font Name を使用することになります。

[^fontinit]: [init(descriptor: UIFontDescriptor, size pointSize: CGFloat)](https://developer.apple.com/documentation/uikit/uifont/1619025-init) というのもあります





# 解説

## 1. Weight と Italic を取得する

`UIFontDescriptor` 経由で取得します。
[`UIFontDescriptor.TraitKey.slant`](https://developer.apple.com/documentation/uikit/uifontdescriptor/traitkey/1616686-slant) で傾き具合を取得できるので、傾きが 0 でない場合に **Italic** とします。

```swift
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. イニシャライズ [^fontinitoriginl]

```swift
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
    }
}
```

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

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

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


```swift
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
        }
    }
}
```

```swift:使用する時
UILabel.appearance().fontFamily = "Avenir"
```




# 環境 / 参考

| Swift | iOS |
| --- | --- |
| 5 | 13 |

- iOS で UIAppearance を使ってグローバルなフォント変更を行う<br>
https://qiita.com/yfujiki/items/7de9421e63dfbfbcc7d4
- Stack Overflow | Font Weight の取得<br>
https://stackoverflow.com/a/53818276
- Apple Documentation | UIFontDescriptor.TraitKey.slant<br>https://developer.apple.com/documentation/uikit/uifontdescriptor/traitkey/1616686-slant
- Apple Documentaion | Adding a Custom Font to Your App<br>https://developer.apple.com/documentation/uikit/text_display_and_fonts/adding_a_custom_font_to_your_app