iOS で日本語文章に発生する中華フォント現象とは

# どういうこと？

iOS アプリの開発をしていると **中国語っぽい日本語フォント** というものを見たことがあると思います。Interface Builder で配置した UILabel だったり、英語設定になっている iOS Simulator でうっかりアプリを実行してしまったときだったり、そのような場面でお目にかかることができるフォントです。Interface Builder の件は残念ながらわかりませんが、シミュレータについては設定アプリから言語設定を「日本語」にすれば本来のヒラギノフォントになります。これはバグではなく iOS の仕様です。詳しくは後ほど説明します。今回はこの **中華フォント現象** の仕組みと、それが通常のアプリでも発現し得ること、そしてその解決策を示したいと思います。

<img src="https://qiita-image-store.s3.amazonaws.com/0/3603/850d3356-22bd-a154-a243-bbee4ef55298.png" alt="compare.png" width=400>

# 中華フォント現象とは

まずは中華フォント現象について詳しく説明します。

## 中華フォントの正体

![heiti.png](https://qiita-image-store.s3.amazonaws.com/0/3603/43c749a5-0e2f-f8d1-480c-43f0ec76e239.png)

このお馴染みになっている宮澤賢治のフレーズと漢字ローマ字の並びが今回の検証にうってつけであったので採用してみました。「て」「ヴ」「底」といった文字には特に違和感がありますね。また「祇」「辻」「鯖」など [JIS X 0213:2004](https://ja.wikipedia.org/wiki/JIS_X_0213#.E4.BE.8B.E7.A4.BA.E5.AD.97.E5.BD.A2.E3.81.AE.E5.A4.89.E6.9B.B4) で字形が変更された漢字もヒラギノの ProN のそれとは異なります。

私はこれを「中華フォント」と勝手に呼んでいますが、別に悪意があるわけではなく、実際に日本語に対して中国語のフォントファミリーが採用されてしまう現象だからです。iOS アプリ開発を長らくやっている方、あるいは外国人の iOS ユーザーで日本語文章を表示したことがある方であれば、この画像のような文字に見覚えがある方は多いと思います。この画像のフォントは **STHeiti** と呼ばれる書体です。試しに Mac の `Font Book.app` で Heiti を検索してみるといくつか出てきます。これは Mac OS に標準で組み込まれている中国語フォントで、iOS も同様です。

ただし、画像のような **STHeiti** は iOS 9 以降の中華フォント現象では現れなくなりました。代わりに別の中国語フォント **PingFang SC** が採用されています。PingFang SC は Apple が開発した中国語向けの書体のようです。システムフォントにおける日本語（ヒラギノ）のように、行高や文字間隔に調整が施されています。

<img width="511" src="https://qiita-image-store.s3.amazonaws.com/0/3603/d90533c7-286a-ccc8-dc18-829b161c3b1c.png">
*macOS High Sierra 10.12 で確認された中華フォント現象*

<img width="592" src="https://qiita-image-store.s3.amazonaws.com/0/3603/7cd36993-bfe7-53d1-15fb-bc7f3c3607f3.png">
*各中国語フォントの比較*

PingFang SC については、こちらに日本語の情報がありました。
[中国フォントではiOS9以降ではPingFang SCを指定必要](https://lxr.co.jp/blog/524/)


## なぜ中華フォント現象が起こるのか

中華フォント現象は iOS の言語設定の仕組みに由来するものです。システムの言語設定の画面を開いてみるとわかるのですが、そこで指定する言語は一種類というわけではなく、あくまで「優先順序」なのです。たとえ日本語のみを指定していたとしても、実は裏側ではアメリカ英語、他の言語、と続いています。英語圏のユーザーであればここには英語がひとつ設定されていると思います。中には2番目、3番目にも何かしらの言語を設定しているユーザーもいるかもしれませんが、それは結局言語の優先度を少し変えているにすぎません。

![s.png](https://qiita-image-store.s3.amazonaws.com/0/3603/4d667433-efe0-34b3-fb61-9c94a5f1cfe7.png)
_言語設定画面_



中華フォント現象は **中国語を日本語よりも優先指定している場合** に起こります。iOS が提供する言語のデフォルトの優先順が何なのかは調べられていませんが、少なくとも言えるのはデフォルトで Chinese の方が Japanese よりも優先されるということです。つまり日本語を優先しない限りは中華フォントが採用されます。このことが原因としてまず挙げられます。そして、なぜだかは知らないけれど組み込みフォントの中で一部の中国語のフォントにも日本語のグリフが含まれているという事実があります。本家本元の日本語フォントを差し置いて中国語フォントの日本語グリフが優先的に採用されてしまう、これが中華フォント現象が起こる仕組みです。そして iOS の仕様でもあります。

おそらく世界中の iOS ユーザーのうち、日本人を除く大半の人の環境で中華フォント現象が起こっていると思われます。なぜなら日本語話者でもない限りはわざわざ日本語を選ぶ必要がないからです。残念ながらほとんどの外国人ユーザーは中華フォントで日本語を見ているか読んでいることになります。

なお、確認できている範囲では以下の言語が日本語よりも優先される場合に中華フォント現象が起こります。どうも台湾の繁体中国語（zh-TW）や朝鮮語（ko）にも日本語を含む別書体が存在するようです。

|言語|コード|
|---|---|
|簡体中国語|zh-Hans|
|繁体中国語|zh-Hant|
|繁体中国語（香港）|zh-HK|
|繁体中国語（台湾）|zh-TW|
|朝鮮語|ko|

# フォントの混植

ローマ字部分と他言語部分（日本語）とで別々のフォントを適用する仕組みです。システムフォントを用いて文章を表示する際、半角のローマ字・数字部分は San Francisco（iOS 8以前/Yosemite では Helvetica Neue）、日本語および全角英数字部分はヒラギノ角ゴシックProNが適用されます。このように2種類以上のフォントを組み合わせることを混植と言います。Adobe Illustrator では合成フォントとも呼ばれます。

そもそもとして中華フォント現象は UIFont で初期化したシステムフォントの日本語部分が言語設定により中華フォントに置き換わってしまうことが原因です。これを無理矢理にでもヒラギノで再合成してあげれば、言語の優先度に関わりなく日本語環境と同じ見た目の日英混植文章を表示することができそうな気がします。

一方でフォントとしてヒラギノをそのまま直指定する方法もありますが、このやり方だと英数字もまとめてヒラギノに置き換わってしまうため、デザインとして使いづらい場面があるでしょう。San Francisco を残すためにもこれは避けたいところです。


# 中華フォント現象を防ぐ方法

シミュレータの中華フォント現象を防ぐ方法がそうであるように、日本語を優先する設定にしてあげる必要があります。ひとつは、ユーザーに言語設定の2番目以降で良いので日本語を中国語よりも上に持ってきてもらうことです。ただしこれは中国語話者の方には無効ですし、そんな面倒な設定をわざわざしてくれる外国人なんてほぼいないでしょう。なのでここはプログラムで解決しましょう。

## UIFontDescriptor

iOS でフォントといえば UIFont ですが、今回は `UIFontDescriptor` というクラスを使用します。UIFontDescriptor では UIFont に加えてフォントの描画のされ方を細かく定義することができます。iOS 7 で追加された Dynamic Type でも馴染みがありますね。

ちなみに Core Text にも `CTFontDescriptor` というものがあります。CoreFoundation(CF*) と Foundation(NS*) の関係と同じように、CTFontDescriptor は UIFontDescriptor / NSFontDescriptor と [toll-free bridge の関係（キャストして変換できる関係）](https://developer.apple.com/library/mac/documentation/StringsTextFonts/Conceptual/CoreText_Programming/Overview/Overview.html#//apple_ref/doc/uid/TP40005533-CH3-SW7)にあります。こちらでも UIFontDescriptor と同等以上のことができると思いますが今回は割愛します。

## ほぼ役に立たない知識

システムフォントは SymbolicTraits に `16384` が設定されているようです。
このようにするか、

```swift
let newDesc = desc.fontDescriptorWithSymbolicTraits(UIFontDescriptorSymbolicTraits(rawValue: 16384))
```

このようにするとシステムフォントと同じ見た目にすることができます。

```swift
let newFontDescriptor = fontDescriptor.fontDescriptorByAddingAttributes([
	UIFontDescriptorTraitsAttribute : [UIFontSymbolicTrait : 16384]
])
```

これで得られる結果は UIFont.systemFontOfSize() と何も変わらないので、あまり実用的ではないでしょう。

## 【失敗】実装

- UIFont でシステムフォントのインスタンスを得る
- UIFontDescriptor でシステムフォントと “Hiragino Sans” を合成する
- UIFontDescriptor から合成済み UIFont のインスタンスを得る

```swift:Swift2.2
let text = "あのイーハトーヴォの\nすきとおった風、\n夏でも底に冷たさをもつ青いそら、\nうつくしい森で飾られたモーリオ市、\n郊外のぎらぎらひかる草の波。\n祇辻飴葛蛸鯖鰯噌庖箸\n底辺直卿蝕薩化\nABCDEFGHIJKLM\nabcdefghijklm\n1234567890"

let fontSize: CGFloat = 20.0
// システムフォント
let systemFont = UIFont.systemFontOfSize(fontSize)
let systemFontDescriptor: UIFontDescriptor = systemFont.fontDescriptor()

// ヒラギノ角ゴシック ProN ファミリーのフォントデスクリプター
let japaneseFontDescriptor = UIFontDescriptor(fontAttributes: [UIFontDescriptorFamilyAttribute : "Hiragino Sans"])

let newFontDescriptor: UIFontDescriptor = systemFontDescriptor.fontDescriptorByAddingAttributes([UIFontDescriptorCascadeListAttribute : [japaneseFontDescriptor]])

let compositeFont = UIFont(descriptor: newFontDescriptor, size: fontSize)

// UILabel に適用
label.font = compositeFont
label.text = text
```

## 【失敗】結果と検証

上記のコードをそれぞれの言語環境で実行した結果が以下の通りです。変化が確認しやすいように「底辺直卿蝕薩化」という漢字も追加してみました。

![system.png](https://qiita-image-store.s3.amazonaws.com/0/3603/240c7019-89d8-c780-dcd5-d43c03dac358.png)

----

![composite.png](https://qiita-image-store.s3.amazonaws.com/0/3603/13f3a732-cf8c-0a4b-8482-bfc0054b2009.png)

システムフォントだとご覧のように中華フォント現象が起こってしまっています。各繁体中国語での句読点の位置は特に酷いことになってしまっていますね。理想としてはシステムフォント版の日本語表示と同等になれば良いのですが、再合成フォント版ではどの言語環境でもそれに近いものとなっています。

しかし残念なことに、システムフォント版の日本語表示と全く同じというわけでもなさそうです。次の画像はシステムフォントと再合成フォントそれぞれの結果を画像化して重ね合わせたものです。日本語部分だけフォントの大きさ、カーニング幅が若干異なることが確認できます。

<img width=375 src="https://qiita-image-store.s3.amazonaws.com/0/3603/703bced8-6621-3230-5dba-be45b2cde01f.png">

これは憶測ですが、欧文書体である San Francisco フォントと日本語書体であるヒラギノをそのまま並べると字の大きさに差ができてしまうため、ヒラギノの方を若干小さくしてカーニング幅にも調整を入れているのだと思われます。大きさに関してはメトリクスを適当に操作して大体 0.94 倍に調整されているということは突き止めましたが、それもヒラギノだけに適用する方法が見つけられませんでしたし、カーニングに至っては正直お手上げです。

## 【正解】無理やり日本語にフォールバックさせる方法

UIFontDescriptorCascadeListAttribute によるヒラギノの合成では完全にシステムフォントを再現することができませんでした。

[WWDC 2013 Session 223 - Using Fonts with Text Kit](https://developer.apple.com/videos/play/wwdc2013/223/) によると、Core Text に定義されている `kCTLanguageAttributeName` を NSAttributedString の属性に指定すればどの言語環境であっても日本語フォントにフォールバックできるとのことです。

<img width="960" src="https://qiita-image-store.s3.amazonaws.com/0/3603/9f7aed7a-7b6a-6e6d-02c6-f0e8428ae634.png">
*WWDC 2013 Session 223 - Using Fonts with Text Kit, #50*

こちらのスライドにも同様の解説があります。
[Handling rich text in Swift](https://speakerdeck.com/kishikawakatsumi/handling-rich-text-in-swift) 

リファレンスには `kCTLanguageAttributeName` について何も書かれていないように見えますが、`CTStringAttributes` に描かれているコメントによると、ロケールIDを指定するとそのロケール固有の禁則処理が適用されるとあります。

実際にこれを検証してみました。

```swift:swift2.2
label.attributedText = attributedString
let systemFont = UIFont.systemFontOfSize(20.0)
let lang = "ja"

let attributedString = NSMutableAttributedString(string: text, attributes: [
	kCTLanguageAttributeName as String : lang,
	NSFontAttributeName : systemFont
	])
```

日本語システムフォント、英語システムフォント、英語＋ja指定、簡体中国語システムフォント、簡体中国語＋ja それぞれの結果が以下の通りです。

![各言語での比較.png](https://qiita-image-store.s3.amazonaws.com/0/3603/3180bb1f-0876-1956-d664-67028242eb1e.png)


---

<img width="375" alt="不一致.png" src="https://qiita-image-store.s3.amazonaws.com/0/3603/66b15a91-3e90-9b79-f4db-17c1e2e07595.png"> <img width="375" alt="一致.png" src="https://qiita-image-store.s3.amazonaws.com/0/3603/2f26cc25-c476-d4f0-feed-73a3639a2ee2.png">

このように、日本語システムフォントと完全一致する結果（右）が得られました。

[検証用プロジェクト](https://github.com/usagimaru/FallbackToJapaneseSystemFont)

# まとめ

iOS で表示する日本語文章は言語環境によって採用されるフォントが変化します。しかもほとんどの言語において、日本語よりも簡体中国語もしくは他の漢字圏のアジア系言語を優先するのでそれらが独自に定義している日本語フォントを採用してしまいます。これを（勝手に）「中華フォント現象」と呼びます。

中華フォント現象は日本語を除くほぼすべての言語環境で発生していると考えられ、iOS の仕組みから仕様として処理せざるを得ません。一つはユーザーが設定を変更すること。各種中国語もしくは朝鮮語よりも日本語を優先する順序にすることです。たとえば最優先が英語であれば2番目に日本語を指定するだけで良いです。ただしこの方法は日本語を除く漢字圏の言語には効果がありません。

もう一つは、アプリ側で NSAttributedString を用意し、属性に `kCTLanguageAttributeName` = `ja` を入れてあげることです。こうすることで日本語ロケールのテキストとして処理されるようになり、非日本語環境であってもシステムフォントのテキストを日本語として表示することができるようになります。たとえ中国語が日本語よりも優先されていたとしてもそれを日本語テキストになるので、中華フォント現象を避けつつ日本語システムフォントのメトリクスを完璧に再現することができます。

以上、中華フォント現象の説明と、その解決策でした。中華フォント現象は日本語環境のユーザーであればあまり馴染みがありませんが、世界向けにアプリケーションを提供する立場として特に日本語書体を重視するという場合がもしあれば、この辺りはよく理解しておいた方が良さそうです。

# 参考資料

- [UIFontDescriptor](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIFontDescriptor_Class/index.html)
- [フォントの生成とカスタマイズ](https://thinkit.co.jp/story/2014/09/02/5199)
- [UIFontDescriptorの属性キー12種の詳細](https://thinkit.co.jp/story/2014/09/09/5200)
-  [Handling rich text in Swift](https://speakerdeck.com/kishikawakatsumi/handling-rich-text-in-swift)