Slide 1

Slide 1 text

SwiftUI Text Rendering Deep Dive Text(“\(price)”)がピクセルになるまで elmetal iOSDC Japan 2026 2026-09-12 #iosdc #d 1

Slide 2

Slide 2 text

About me @el̲metal̲ iOS App Developer 複数チームの 技術支援 #iosdc #d 2 競馬 対戦ゲーム 育児

Slide 3

Slide 3 text

Intro #iosdc #d 3

Slide 4

Slide 4 text

Textは簡単ではない • 普段何気なく使っているText • 簡単ではない • ロケールを変えるとレイアウトが崩れる • TextとStringでローカライズの挙動が異なる • AttributedStringの一部だけスタイルを変えたい • テキストエフェクトをカスタムしたい #iosdc #d 4

Slide 5

Slide 5 text

値が画面に表示されるまで Value 画面への表示 Formatting Text Construction Layout #iosdc #d 5 表示内容の構築 Rendering Pixels

Slide 6

Slide 6 text

FormatStyleとStringInterpolation #iosdc #d 6

Slide 7

Slide 7 text

FormatStyleとStringInterpolation Value 画面への表示 Formatting Text Construction Layout #iosdc #d 7 表示内容の構築 Rendering Pixels

Slide 8

Slide 8 text

FormatStyle #iosdc #d 8

Slide 9

Slide 9 text

Textは同じ表示を複数の書き方で作れる Text("¥1,234") Text(verbatim: "¥1,234") Text(price, format: .currency(code: "JPY”)) Text(verbatim: price.formatted(.currency(code: "JPY"))) #iosdc #d 9

Slide 10

Slide 10 text

Textは同じ表示を複数の書き方で作れる Text("¥1,234") これらはTextに渡す値を 作る途中経過が異なる Text(verbatim: "¥1,234") Text(price, format: .currency(code: "JPY”)) Text(verbatim: price.formatted(.currency(code: "JPY"))) #iosdc #d 10

Slide 11

Slide 11 text

文字列リテラルを渡す Text(verbatim: "¥1,234") • これは単純なStringを渡している • LocalizedStringKeyやLocalizedStringResourceなどのリソースとして扱わ ず、そのままTextに渡される #iosdc #d 11

Slide 12

Slide 12 text

文字列リテラルを渡す Text("¥1,234") • これは単純なStringを渡しているとは限らない • LocalizedStringKeyやLocalizedStringResourceなどのリソースとして扱われ Textに渡される場合がある #iosdc #d 12

Slide 13

Slide 13 text

FormatStyleで作った文字列を渡す Text(verbatim: price.formatted(.currency(code: "JPY"))) • FoundationのFormatStyleで表示文字列を生成 • フォーマット処理はTextをinitする前に完了 #iosdc #d 13

Slide 14

Slide 14 text

値とFormatStyleをTextへ渡す Text(price, format: .currency(code: "JPY")) • Textの中でフォーマットが行われる • SwiftUIのenvironmentを使ってLocaleを表示に反映できる • 先にフォーマットする場合はFormatStyleのlocaleが使われるので、SwiftUI のenvironmentではLocaleが反映されない #iosdc #d 14

Slide 15

Slide 15 text

Localeを変えて表示してみる ja̲JP Text(price, format: .currency(code: “JPY")) .environment(\.locale, Locale(identifier: "ja_JP")) #iosdc #d 15

Slide 16

Slide 16 text

Localeを変えて表示してみる en̲US Text(price, format: .currency(code: “JPY")) .environment(\.locale, Locale(identifier: “en_US”)) #iosdc #d 16

Slide 17

Slide 17 text

Localeを変えて表示してみる fr̲FR Text(price, format: .currency(code: “JPY")) .environment(\.locale, Locale(identifier: “fr_FR”)) #iosdc #d 17

Slide 18

Slide 18 text

StringInterpolation #iosdc #d 18

Slide 19

Slide 19 text

SwiftのInterpolation • ExpressibleByStringInterpolationに準拠する型は独自のStringInterpolaiton を持てる • String→String.StringInterpolation • Text→Text.StringInterpolaiton • リテラルが最終的にどの型で解釈されるかによってどのStringInterpolationが 使われるかが決まる #iosdc #d 19

Slide 20

Slide 20 text

String.StringInterpolation let s: String = "価格は\(price)です" • 型をStringに明示しているのでStringで扱われる • この場合はString.Interpolationが使われる #iosdc #d 20

Slide 21

Slide 21 text

Text.StringInterpolation Text("価格は\(price, format: .currency(code: "JPY"))です") • リテラルはText.StringInterpolationで処理される • Text.StringInterpolationにより、「価格は¥1,234です」全体を一つのローカライズ単位と して扱える • 翻訳リソース側では値をプレースホルダーとして動的に組み込める • 文全体の表現や語順を変更できる • TextのInterpolationに値とFormatStyleを渡すことで、リテラル、値、FormatStyleを統合 して、localizable textにできる #iosdc #d 21

Slide 22

Slide 22 text

LocaleとICUによる国際化処理 #iosdc #d 22

Slide 23

Slide 23 text

LocaleとICUによる国際化処理 Value 画面への表示 Formatting Text Construction Layout #iosdc #d 23 表示内容の構築 Rendering Pixels

Slide 24

Slide 24 text

FormatStyleは1234という値から ¥1,234という文字列をどのように作るのか? #iosdc #d 24

Slide 25

Slide 25 text

通貨フォーマットを支えるもの FormatStyleのCon gurationとLocaleをもとにICUが表示規則を適用 1234 ICU 表示規則を適用 • FormatStyleのCon guration .currency(code: “JPY”) • Locale ja̲JP #iosdc #d fi fi 25 ¥1,234

Slide 26

Slide 26 text

Locale • Localeは単なる言語の指定ではない • 言語や地域、数字の体系、暦などの国際化処理で表示方法を決めるための複 数の要素をまとめたコンテキスト情報 • Localeに書式が埋め込まれているわけではない • ICUが適切なロケールデータを解決できるようにコンテキストを提供する #iosdc #d 26

Slide 27

Slide 27 text

FormatStyleのCon guration • Con gurationでは値の表現方法を精度・桁区切り・符号などの要素ごとに指定する • 例えばNumberFormatStyleCon guration • currency: JPY • Precision: integer • Grouping: automatic • Sign: automatic • 表現方法の指定から具体的な記号がどうやって決まるのか? #iosdc #d fi fi fi 27

Slide 28

Slide 28 text

ICU(International Components for Unicode) Unicode対応の文字列処理や、Localeに応じた書式設定を提供する国際化ライブラリ • Swift Foundationの国際化ではICUが利用されている • FormatStyleの設定とLocaleを使ってICU number formatterを構成する • ICUが通過記号・区切り・配置などをLocaleに応じて解決 • Swift FoundationではICUNumberFormatter.swiftから実装を追える • 実装の詳細は省略 • ここで表示用の文字列に変換される #iosdc #d 28

Slide 29

Slide 29 text

AttributedStringとText #iosdc #d 29

Slide 30

Slide 30 text

AttributedString Value 画面への表示 Formatting Text Construction Layout #iosdc #d 30 表示内容の構築 Rendering Pixels

Slide 31

Slide 31 text

AttributedString 文字列の範囲ごとに属性を持てる値型 ¥1,234 #iosdc #d 31

Slide 32

Slide 32 text

AttributedString 文字列の範囲ごとに属性を持てる値型 ¥ currency symbol 1 integer , grouping separater 234 integer #iosdc #d 32

Slide 33

Slide 33 text

AttributedScopes 用途ごとに属性を整理 • AttributedStringで扱える属性は見た目だけではない • AttributedScopesで用途ごとに属性が整理されている • FoundationAttributes • SwiftUIAttributes / UIKitAttributes / AppKitAttributes • Custom Attributes • 型でスコープを分けることで属性名の衝突を避けて属性追加ができる #iosdc #d 33

Slide 34

Slide 34 text

AttributedString.Runs • AttributedString.Runsで属性が一定範囲ごとに分割される • 隣り合う文字が同じ属性を持っていれば一つのrunにまとまる ¥ currency symbol 1 integer , grouping separater 234 integer #iosdc #d 34

Slide 35

Slide 35 text

AttributedStringとFormatStyle • 一部のFormatStyleはAttributedStringの結果を返せる • AttributedStringを返す仕組みを使うことでフォーマットの構造を再度解析す ることなく扱える #iosdc #d 35

Slide 36

Slide 36 text

FormatStyleでAttributedStringを返す let attributed = price.formatted( .currency(code: "JPY").attributed ) for run in attributed.runs { print(attributed[run.range]) print(run.attributes) } #iosdc #d 36

Slide 37

Slide 37 text

FormatStyleでAttributedStringを返す let attributed = price.formatted( .currency(code: "JPY").attributed ) ¥ currency symbol 1 integer for run in attributed.runs { print(attributed[run.range]) print(run.attributes) } , grouping separater 234 #iosdc #d 37 integer

Slide 38

Slide 38 text

TextとAttributedString • TextはAttributedStringも受け取れる • AttributedStringを渡すと、AttributedStringの属性に従ってテキストがスタ イリングされる #iosdc #d 38

Slide 39

Slide 39 text

SwiftUIのテキストレイアウト #iosdc #d 39

Slide 40

Slide 40 text

SwiftUIのテキストレイアウト Value 画面への表示 Formatting Text Construction Layout #iosdc #d 40 表示内容の構築 Rendering Pixels

Slide 41

Slide 41 text

Textのサイズ • Text(“hello world”)が何行になるかはコードだけでは決まらない • 利用できる幅が広ければ一行に • 狭ければ複数行に分かれる #iosdc #d 41

Slide 42

Slide 42 text

Textのサイズは文字列だけでは決まらない フォントファミリー 利用できる幅 サイズ・ウェイト 行間 #iosdc #d 42 行数制限

Slide 43

Slide 43 text

A text view always uses exactly the amount of space it needs to display its rendered contents, but you can a ect the viewʼs layout. Text ̶ https://developer.apple.com/documentation/swiftui/text #iosdc #d ff 43

Slide 44

Slide 44 text

SwiftUIのレイアウトプロセス 親Viewが利用可能サイズを子Viewに提示する 子Viewが提案に対して自分のサイズを親Viewに返す 親Viewが子Viewを配置する SwiftUIでカスタムビューを構築する - WWDC19 - https://developer.apple.com/jp/videos/play/wwdc2019/237 #iosdc #d 44

Slide 45

Slide 45 text

Textのレイアウトプロセス 基本はSwiftUIのレイアウトプロセスに従う 親Viewが利用可能サイズをTextに提示する 利用可能幅に合わせて行を形成する 各行を配置し、Textのサイズを親Viewに返す #iosdc #d 45

Slide 46

Slide 46 text

Textのレイアウト結果に影響する属性 タイポグラフィ Font ローカライズ Locale Layout direction Dynamic type size Line spacing Line limit Multiline text alignment 段落設定 Available width Truncation #iosdc #d 46 表示制約

Slide 47

Slide 47 text

レイアウト結果への影響の例 文字とグリフが1対1にならない • 複数のunicode scalarが一つの文字を構成する • 複数の文字が合字によって一つのグリフになる • 前後の文字に応じてグリフが変わる • 使用するフォントによってグリフの形や幅が変わる #iosdc #d 47

Slide 48

Slide 48 text

レイアウト結果への影響の例 文字列中の順序と画面上の並びが一致しない • Right to Leftの文字が含まれる • 一つの文章内で異なる書字方向が混在する #iosdc #d 48

Slide 49

Slide 49 text

シェーピング 文字列をフォントに応じたグリフ列に変換する 文字列 フォント情報 シェーピング • グリフの選択 • 位置の調整 #iosdc #d 49 グリフ列 グリフ同士の配置

Slide 50

Slide 50 text

シェーピング結果にレイアウト条件を適用する シェーピング済みのグリフ レイアウト条件 • 利用可能なサイズ・改行規則・書字方向 • lineLimit・truncationMode Text.Layout #iosdc #d 50

Slide 51

Slide 51 text

Text.Layout • Text.Layoutはレイアウト済みテキ ストの結果 Text.Layout ├─ Line │ ├─ Run │ │ ├─ Glyph │ │ └─ Glyph │ └─ Run └─ Line • 複数のLineで構成され、各Line は複数のRunを持つ • Runにはシェーピングされ、位 置が決まったGlyphが含まれる • TextRendererがこの構造を走査し て描画する #iosdc #d 51

Slide 52

Slide 52 text

Text.Layout.Line • Text.LayoutはLineのコレクション • Lineは一行に配置された複数のRunを持つ • originから行の描画位置を参照できる • typographicBoundsから幅・ascent・descent・leadingを参照できる #iosdc #d

Slide 53

Slide 53 text

Text.Layout.Run • シェーピング後のグリフはText.Layout.Runにまとめられる • フォント・属性・書字方向が変わる位置でRunが分かれる • Text.Layout.RunとAttributedString.Runの境界は一致しない • AttributedStringの属性以外もRunの境界に影響する #iosdc #d 53

Slide 54

Slide 54 text

TextRendererと描画 #iosdc #d 54

Slide 55

Slide 55 text

TextRendererと描画 Value 画面への表示 Formatting Text Construction Layout #iosdc #d 55 表示内容の構築 Rendering Pixels

Slide 56

Slide 56 text

TextRenderer SwiftUIのTextの描画を置き換えるプロトコル • レイアウト済みのText.Layoutを受け取る • GraphicsContextへ描画する • .textRenderer(̲:)をつけたView階層内のTextに適用される SwiftUIによるカスタムビジュアルエフェクトの作成 - WWDC24 - https://developer.apple.com/jp/videos/play/wwdc2024/10151/ #iosdc #d 56

Slide 57

Slide 57 text

draw(layout: in:) レイアウト結果と描画先を受け取る • layoutからLine・Run・RunSlice を走査する func draw( layout: Text.Layout, in context: inout GraphicsContext ) • contextへ各要素を描画する • TextRendererはレイアウト結果 を描画するだけで、再レイアウト はしない SwiftUIによるカスタムビジュアルエフェクトの作成 - WWDC24 - https://developer.apple.com/jp/videos/play/wwdc2024/10151/ #iosdc #d 57

Slide 58

Slide 58 text

描画する単位 Text.Layoutを走査 • Lineを描画すると、行単位で効 果を適用できる Text.Layout ├─ Line │ ├─ Run │ │ ├─ RunSlice │ │ └─ RunSlice │ └─ Run └─ Line • Runを描画すると、属性のまとま りごとに処理できる • RunSliceを利用すると、グリフ や埋め込み要素ごとに処理でき る #iosdc #d 58

Slide 59

Slide 59 text

RunをGraphicsContextに描画する 配置済みのRunはタイポグラフィ情報を持つ • TextRendererではText.Layout.RunをGraphicsContextに直接描画できる • Runには描画に利用できるタイポグラフィ情報も含まれている • typographicBoundsから width, ascent, rectなどを取得できる #iosdc #d 59

Slide 60

Slide 60 text

RunをGraphicsContextに描画する typographicBoundsを参照して描画する struct BoundsTextRenderer: TextRenderer { func draw( layout: Text.Layout, in context: inout GraphicsContext ) { for line in layout { for run in line { let rect = run.typographicBounds.rect context.stroke( Path(rect), with: .color(.red), lineWidth: 1 ) context.draw(run) } } } } #iosdc #d 60

Slide 61

Slide 61 text

TextAttributeで描画を変更したいRunを指定する • TextAttributeはTextにつけるカスタム属性 • TextRendererはRunからTextAttributeを参照できる • TextAttribute自体は見た目を変更しない • TextRendererでTextAttributeを条件に描画方法を変更できる • テキストの内容やローカライズによってrunの分割が変わっても TextAttributeを参照することで視覚効果をテキスト内容と分離して扱える #iosdc #d 61

Slide 62

Slide 62 text

AttributedStringとTextAttributeの違い AttributedString TextAttribute 対象 文字列の範囲 Textの範囲 主な用途 内容と書式を保持する TextRendererへ対象の情報を渡す レイアウトへの影響 フォントなどの属性が影響する 属性そのものはレイアウトを変更しない 描画 Textの描画に反映される TextRendererが解釈すると反映される #iosdc #d 62

Slide 63

Slide 63 text

描画が元の領域をはみ出す場合 • Shadow、ぼかし、移動はレイアウト済み領域の外まで描画される • 描画効果を加えても、元のTextのレイアウト領域は自動では広がらない • 必要に応じてRenderer側で表示の余白やサイズを考慮する • displayPaddingで領域外へ広がる描画が切れないようにする • Textの折り返しや配置は変わらない • View.paddingはレイアウト上の余白を扱うので少し役割が異なる #iosdc #d 63

Slide 64

Slide 64 text

TextRendererはレイアウト結果を描画する • SwiftUIからレイアウト済みのText.Layoutを受け取る • TextRendererはLineやRunを走査して、GraphicsContextへ描画する • 描画は変更できるが元のレイアウト結果は変わらない #iosdc #d 64

Slide 65

Slide 65 text

まとめ #iosdc #d 65

Slide 66

Slide 66 text

TextがPixelになるまで Value 画面への表示 Formatting Text Construction Layout #iosdc #d 66 表示内容の構築 Rendering Pixels

Slide 67

Slide 67 text

Textの見た目は段階ごとに決まる • 表示内容を変える • FormatStyle/AttributedString • 配置を変える • Layout • 描画を変える • TextRenderer

Slide 68

Slide 68 text

ありがとうございました #iosdc #d