中華フォントをたった1行で解決するtypesettingLanguage【SwiftUI】

![中華フォント例](https://camo.qiitausercontent.com/d36bf81387b691e11cd26b8e922bf21ac9e71f61/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f333536343636322f63353932396233302d373932392d333737302d646138322d6261396164376662393466362e706e67)

## 中華フォントとは
中華フォントとは、デバイスの言語設定で日本語を選択していないような場面において、日本語文章(特に漢字)のフォントが中華圏基準のものになる現象の俗称です。

![比較](https://camo.qiitausercontent.com/8c05c570f8f115e50531f5de8a1401e1a2427d70/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f333536343636322f61653834303138392d653962392d646234362d323166372d6638316437303830303764362e706e67)

___注記___
_「中華フォント」という呼称がポリティカルコレクトネス的に適切か否かを私個人としては判断しかねるため、本記事では以降「中華フォント(俗称)」と呼びます。_

## typesettingLanguageとは
特定の言語/書体/地域の表現を尊重するように明示的に指定するSwiftUIのViewModifierです。iOS 17世代以降の全てのAppleプラットフォームで対応しています。

> In some cases Text may contain text of a particular language which doesn’t match the device UI language. In that case it’s useful to specify a language so line height, line breaking and spacing will respect the script used for that language.

https://developer.apple.com/documentation/swiftui/view/typesettinglanguage(_:isenabled:)-4ldzm

https://developer.apple.com/documentation/swiftui/view/typesettinglanguage(_:isenabled:)-39brr

### UIKit
UIKitにも同等のAPIがあるようです。

https://developer.apple.com/documentation/uikit/uitraitoverrides/4213133-typesettinglanguage

## 実践
![例.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3564662/3d9288dc-f3e4-c55b-3dbd-d4a814218a61.png)

```swift
Text("今すぐ直角に変えてください。")
Text("今すぐ直角に変えてください。")
    .typesettingLanguage(.init(languageCode: .japanese))
```

## 中華フォント(俗称)以外での事例
「同じ文字列なのに特定の言語/書体/地域によってフォント表現が変わる現象」は中華フォント(俗称)に限りません。

### ロシア語とブルガリア語の「ニコライ」
ニコライ(Николай)という言葉も言語によって表記が変わります。

![ニコライ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3564662/86d79cdb-658e-6f2b-9151-dd2aac31dacf.png)

```swift
Text(verbatim: "Николай")
    .typesettingLanguage(.init(languageCode: .russian))
Text(verbatim: "Николай")
    .typesettingLanguage(.init(languageCode: .bulgarian))
```

どちらもUnicode的には`U+041D 0438 043A 043E 043B 0430 0439`です。

### タイ語などは縦に長い文字型
英文の中に固有名詞としてタイ語のような縦に長い文字型を混在させると行の高さのミスマッチが起きるケースがあります。typesettingLanguageで行の高さ調整も実現出来ます。

![thai.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3564662/121ed378-5eea-6830-de59-b5700698e366.png)

```swift
Text(verbatim: "Who's a good dog, ไมโล?")
    .typesettingLanguage(.init(languageCode: .english))
Text("""
    Who's a good dog, \
    \(Text("ไมโล").typesettingLanguage(.init(languageCode: .thai)))?
    """)
```

### 中華フォント(俗称)の逆
もちろん中華フォント(俗称)の逆もあるわけです。例えば「日本語しか対応していないアプリを対象言語圏の人達が使う場合」や「日本人が対象言語習得を目的とした学習アプリを使う場合」などです。

![返.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3564662/37a56a91-0f5a-2664-3ef6-9faf16aeb35c.png)

```swift
List(Array<Locale.Language>([
    .init(languageCode: .chinese, script: .hanSimplified),
    .init(languageCode: .chinese, script: .hanTraditional),
    .init(languageCode: .chinese, script: .hanTraditional, region: .hongKong),
    .init(languageCode: .korean),
    .init(languageCode: .japanese)
]), id: \.self) {
    Text(verbatim: "返")
        .typesettingLanguage($0)
        .badge($0.languageCode?.debugDescription)
        .badge($0.script?.debugDescription)
        .badge($0.region?.debugDescription)
}
```

## typesettingLanguageの引数
```swift
.typesettingLanguage(Locale.Language(identifier: "ja"))
.typesettingLanguage(Locale.Language(languageCode: .japanese))
.typesettingLanguage(Locale.Language(script: .japanese))
.typesettingLanguage(Locale.Language(region: .japan))
.typesettingLanguage(Locale.Language(components: /* 略 */))
.typesettingLanguage(TypesettingLanguage.explicit(.init(languageCode: .japanese)))
```

様々なパラメータを選択できます。
TypesettingLanguage.explicitの有無で何が変わるのかは私個人の理解力では不明。

## Textに渡す値の型について
```swift
Text(verbatim: "返")
    .typesettingLanguage(.init(languageCode: .japanese))
Text("返") // ← これだと失敗する?
    .typesettingLanguage(.init(languageCode: .japanese))
```

Textに渡す値の型によってtypesettingLanguageが適用されたりされなかったりします。型を指定せずに文字列リテラルを渡すとLocalizedStringKey等に型推論されるのでそれ関係で挙動が変わるのだと思います。
バグかどうかは分かりませんが、実際に実装する際にはTextに渡す値の型には注意した方がよさそうです。

## iOS 17世代未満は？
### iOS 15世代以降、iOS 17世代未満はAttributedStringでこんな感じ
```swift
struct 日本語テキスト: View {
    var 文字列: String
    private var attributedContent: AttributedString {
        var 値 = AttributedString(stringLiteral: self.文字列)
        値.languageIdentifier = "ja"
        return 値
    }
    var body: some View {
        Text(self.attributedContent)
    }
}
```

### それ以前はNSAttributedStringでこんな感じ
```swift
class ViewController: UIViewController {
	@IBOutlet weak var label: UILabel!
	override func viewDidLoad() {
		super.viewDidLoad()
        let nsMutableAttributedString = NSMutableAttributedString(string: /* 略 */, attributes: [
            kCTLanguageAttributeName as NSAttributedString.Key : "ja",
        ])      
		label.attributedText = nsMutableAttributedString
	}
	//色々省略
}
```

### 詳しくはこちらを参照
https://qiita.com/mjnfhbuvwebwfiejcnw/items/c9961f8ba6c366953752

## まとめ
- typesettingLanguageという新APIがとても良い感じ
- 特定の言語/書体/地域の表現を尊重するように明示的に指定することがたった1行で可能
- iOS 17世代以降の全てのAppleプラットフォームに対応

## 関連リンク
https://developer.apple.com/wwdc23/10148

https://developer.apple.com/wwdc23/10055

https://developer.apple.com/wwdc23/10058

https://qiita.com/usagimaru/items/46c27c42e862fb8aba7e

https://tonsky.me/blog/unicode/

https://qiita.com/mjnfhbuvwebwfiejcnw/items/c9961f8ba6c366953752

https://developer.apple.com/documentation/swiftui/view/typesettinglanguage(_:isenabled:)-4ldzm

https://developer.apple.com/documentation/swiftui/view/typesettinglanguage(_:isenabled:)-39brr

https://developer.apple.com/documentation/uikit/uitraitoverrides/4213133-typesettinglanguage