ObjecTips

Swift & Objective-C で iOS とか macOS とか

ヒラギノフォントが切れる問題 SwiftUI編

検証環境 Xcode 11.4.1

iOS+ヒラギノ+UILabel とか UIButton でググると過去の UIKit での問題が参照できます。
この問題は SwiftUI でも発生します。

まずサンプルとしてヒラギノ角ゴのW3を指定して Text を作成。
(デバッグのため青色の枠線も表示)

Japanese font without Japanese character causes th ...

f:id:Koze:20200509011329p:plain:w536

上の日本語を含まない状態だとpとyの下が切れている。
下の日本語を含んだ状態だと切れずに表示されている。


試しに frame を設定して Text の表示領域を大きくしてみる。*1

    var body: some View {
        VStack(spacing: 10) {
            Text("Copy")
                .font(font)
                .frame(height: 60)
                .border(borderColor)

            Text("Copy")
                .font(font)
                .border(borderColor)
                .frame(height: 60)
                .border(borderColor)
        }
    }

f:id:Koze:20200509013020p:plain:w530

表示領域は大きくなっても文字は切れたまま。


次は TextbaselineOffset を試してみる。*2

    let font = Font.custom("HiraginoSans-W3", size: 50)
    let uiFont = UIFont(name: "HiraginoSans-W3", size: 50)!
    let borderColor = Color(.systemBlue)

    var body: some View {
        HStack(spacing: 10) {
            Text("Copy")
                .font(font)
                .border(borderColor)
            
            Text("Copy")
                .font(font)
                .baselineOffset(-uiFont.descender)
                .border(borderColor)
        }
    }

f:id:Koze:20200509015209p:plain:w520

文字が切れなくなった。


SwiftUI の Font からはフォント情報の descender を取得する事ができないため別途 UIFont を使っている。これを CTFont を使う様にリファクタリング。
UIFontdescender は正の値が入っているが CTFontdescender は負の値が入っているので baselineOffset で使う際の符号も修正している。

    let ctFont = CTFontCreateWithName("HiraginoSans-W3" as CFString, 50, nil)
    var ctFontDescender: CGFloat {
        CTFontGetDescent(ctFont)
    }
    let borderColor = Color(.systemBlue)

    var body: some View {
        HStack(spacing: 10) {
            Text("Copy")
                .font(.init(ctFont))
                .border(borderColor)
            
            Text("Copy")
                .font(.init(ctFont))
                .baselineOffset(ctFontDescender)
                .border(borderColor)
        }
    }

f:id:Koze:20200509020338p:plain:w508

見た目の結果は1つ前の実装と同じ。
でも今回の実装の場合 Text の箇所で Font を都度 CTFont から init するため、表示箇所が多い場合はFontUIFont を1つずつ作成する前回の実装の方が動作としては効率的な気もする。
コードの構造的には今回の CTFont を使った実装の方が整頓されていると思う。


現在の実装では baselineOffset で調整した分テキスト全体が上に移動するため、表示文字や周りのUIとの組み合わせによっては見た目が気になってくる。

f:id:Koze:20200509021829p:plain:w520
(↑文字全体が上にシフトしているのが気になる。日本語の場合特に。)

SwiftUI には便利な offsetpadding が存在するので、これらで baselineOffset による移動を再調整してやる。

offset

                Text("Copy")
                    .font(.init(ctFont))
                    .baselineOffset(ctFontDescender)
                    .border(Color(.systemRed))
                    .offset(y: ctFontDescender / 2)
                    .border(borderColor)

f:id:Koze:20200509070053p:plain:w498

padding

                Text("Copy")
                    .font(.init(ctFont))
                    .baselineOffset(ctFontDescender)
                    .border(Color(.systemRed))
                    .padding(.top, ctFontDescender)
                    .border(borderColor)

f:id:Koze:20200509070119p:plain:w500

これで見た目の問題も解消された。


最後にお好みでExtension化。
毎回の font baselineOffset offset or padding の操作を Extension にしてまとめてやることもできる。*3

extension Text {
    public func ctFont(_ ctFont: CTFont) -> some View {
        let descent = CTFontGetDescent(ctFont)
        return self.font(.init(ctFont))
            .baselineOffset(descent)
            .offset(y: descent / 2)
//            .padding(.top, ctFontDescender)
    }
}

この Extension の中で条件分岐して日本語フォント(もしくは問題のある特定のフォント)の場合のみ処理を入れて他のフォント利用時にはレイアウトに影響を与えない様にする事も検討できる。

extension CTFont {
    
    // quick example
    var isJapaneseFont1: Bool {
        let encoding = CTFontGetStringEncoding(self)
        return encoding == CFStringEncodings.macJapanese.rawValue
    }
    
    // quick example
    var isJapaneseFont2: Bool {
        let languages = CTFontCopySupportedLanguages(self) as? [String]
        return languages?.contains("ja") ?? false
    }
}

上記の日本語フォントの判定実装はちゃんと調査はしてないので取り急ぎの例として。

最終実装

Japanese font without Japanese character causes th ...

f:id:Koze:20200509070543p:plain:w498

*1:UIKit だと frame を大きく取る事で問題を解決できる。

*2:UIKit だと NSAttributedString の baselineOffset を使って問題を解決できる。

*3:SwiftUI のビルトインの Text Extension の実装を見ていると返り値で Text ではなくて some View を返してやるのはイレギュラーな感じはする。