try! Swift Tokyo 2025で以下youtubeにも公開されているセッションにてTextRenderer
が紹介されていました。
これを見て、テキストに装飾をつけるのかっこいい!と思い、自分でも試したみたくなったので、今回はTextRenderer
を試したみた内容を記事にしてみました。
TextRenderer
は SwiftUI のText
の描画処理を制御することで Effect を付与できる APIです。
以下のように、簡単にText
を装飾することができます。
import SwiftUI
struct ColorfulTextRenderer: TextRenderer {
func draw(layout: Text.Layout, in ctx: inout GraphicsContext) {
for line in layout {
for run in line {
for (index, grif) in run.enumerated() {
var copy = ctx
let degree = Angle.degrees(360 / Double(index + 1))
copy.addFilter(.hueRotation(degree))
copy.draw(grif)
}
}
}
}
struct Attribute: TextAttribute {}
}
#Preview {
Text("Hello World!!!")
.font(.largeTitle)
.foregroundStyle(.red)
.textRenderer(ColorfulTextRenderer())
}
ざっくりの手順は以下の通りです。
こう見るとTextRenderer
さえ定義してしまえば、あとはText
に modifier をつけるだけなので簡単そうです。
-
TextRenderer
に準拠した型を定義 -
Text
に対して、定義したカスタムのTextRenderer
をtextRenderer
modifier を使って指定する
順に詳細に解説していきます。
TextRenderer
に準拠した型を定義
TextRenderer
に準拠するには、drawメソッドを定義する必要があります。
最低限、以下のようにすれば、TextRenderer
に準拠したカスタムのTextRenderer
は定義できます。
struct SampleTextRenderer: TextRenderer {
func draw(layout: Text.Layout, in ctx: inout GraphicsContext) {}
}
draw
メソッドにはText.Layout
とGraphicsContext
という型の引数をとり、この引数を使うことで、テキストを描画したり描画するテキストに Effect を付与することができる。
Text.Layout
とは
Text.Layout
にはレンダリングするテキストの情報が格納されている。
※ 大変わかりやすかったので、こちらの記事より引用させていただきました。
Line、Run、RunSlice の内容は以下の通りです。
- Line: テキストレイアウト内の 1 行を表す
- Run: 同じ属性(フォント、色、スタイルなど)を持つ文字の連続した並びを表す
- RunSlice: 文字(テキストグリフ)を表す
以下のようにすることで、Line、Run、RunSlice を取得することができます。
func draw(layout: Text.Layout, in ctx: inout GraphicsContext) {
for line in layout { // lineの取得
for run in line { // runの取得
for runSlice in run { // runSliceの取得
}
}
}
}
GraphicsContext
とは
その名の通り、画像描画のコンテキストを表す構造体で、描画先の状態(座標変換・フィルタ・描画スタイルなど)を保持しています。TextRenderer
においてGraphicsContext
には、テキストを描画したり描画するテキストに Effect を与える役割を持ちます。
テキストを描画するときは、以下のようにdraw
メソッドを呼び出します。
以下例ではdraw
メソッドに line を指定しているが、Line 単位だけでなく、Run や RunSlice を指定することで任意の単位でテキストを描画できます。
func draw(layout: Text.Layout, in ctx: inout GraphicsContext) {
for line in layout {
ctx.draw(line)
}
}
以下のようにしても、上記と同じく全てのテキストが描画されます。
func draw(layout: Text.Layout, in ctx: inout GraphicsContext) {
for line in layout {
for run in line {
for runSlice in run {
ctx.draw(runSlice)
}
}
}
}
描画するテキストに Effect を与えるには、draw
メソッドでテキストを指定する前に、GraphicsContext
に適用した Effect を以下のように設定します。
以下例では、後の文字になる程文字の Y 軸に拡大させて、影をつける Effect を設定しました。
func draw(layout: Text.Layout, in ctx: inout GraphicsContext) {
for line in layout {
for run in line {
for (index, runSlice) in run.enumerated() {
var copy = ctx
copy.scaleBy(x: 1, y: (CGFloat(index) * 0.1 + 1.0))
copy.addFilter(.shadow(radius: 2, x: 3, y: 3))
copy.draw(runSlice)
}
}
}
}
上記を実際に使用した例は以下の通りです。
struct SampleTextRenderer: TextRenderer {
func draw(layout: Text.Layout, in ctx: inout GraphicsContext) {
for line in layout {
for run in line {
for (index, runSlice) in run.enumerated() {
var copy = ctx
copy.scaleBy(x: 1, y: (CGFloat(index) * 0.1 + 1.0))
copy.addFilter(.shadow(radius: 2, x: 3, y: 3))
copy.draw(runSlice)
}
}
}
}
}
#Preview {
Text("Hello World!!!")
.font(.largeTitle)
.textRenderer(SampleTextRenderer())
}
ちなみにvar copy = ctx
とわざわざdraw
メソッドの引数のctx
をcopy
に代入しているのかというと、副作用を防ぐためです。GraphicsContext
には状態(座標変換・フィルタ・描画スタイルなど)を保持しており、ctx
はinout
で渡されるため for ループで全ての Effect 設定をctx
で行うと全てのテキストに重複して設定されてしまうため、意図した表示になりません。
例えば、以下で表示のされ方が変わります。
func draw(layout: Text.Layout, in ctx: inout GraphicsContext) {
for line in layout {
for run in line {
for (index, runSlice) in run.enumerated() {
ctx.opacity = CGFloat(index) * 0.1 + 0.1
ctx.addFilter(.shadow(radius: 1, x: 2, y: 2))
ctx.draw(runSlice)
}
}
}
}
影の Effect がループ毎に重複されるので、後になるほど影が濃くなる
func draw(layout: Text.Layout, in ctx: inout GraphicsContext) {
for line in layout {
for run in line {
for (index, runSlice) in run.enumerated() {
var copy = ctx
copy.opacity = CGFloat(index) * 0.1 + 0.1
copy.addFilter(.shadow(radius: 1, x: 2, y: 2))
copy.draw(runSlice)
}
}
}
}
テキストに Effect を設定する例として、今回は一部のものしか使用しておらず、他にも Effect を付与するためのメソッドがあるので、手元でも色々試してもらえると楽しくなると思います!
GraphicsContext
の API ドキュメントより、どんな Effect の設定ができるのかが確認できます。
いかがだったでしょうか!少しでもTextRenderer
によるテキストの装飾に興味を持っていただけたら幸いです。
また内容に誤りなどありましたら、ご指摘いただけると幸いです!