TextRendererでSwiftUIのTextを装飾を試してみる #iOS - Qiita

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 をつけるだけなので簡単そうです。

  1. TextRendererに準拠した型を定義
  2. Textに対して、定義したカスタムのTextRenderertextRenderermodifier を使って指定する

順に詳細に解説していきます。

TextRendererに準拠した型を定義

TextRendererに準拠するには、drawメソッドを定義する必要があります。

最低限、以下のようにすれば、TextRendererに準拠したカスタムのTextRendererは定義できます。

struct SampleTextRenderer: TextRenderer {

    func draw(layout: Text.Layout, in ctx: inout GraphicsContext) {}
}

drawメソッドにはText.LayoutGraphicsContextという型の引数をとり、この引数を使うことで、テキストを描画したり描画するテキストに Effect を付与することができる。

Text.Layoutとは

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メソッドの引数のctxcopyに代入しているのかというと、副作用を防ぐためです。
GraphicsContextには状態(座標変換・フィルタ・描画スタイルなど)を保持しており、ctxinoutで渡されるため 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によるテキストの装飾に興味を持っていただけたら幸いです。
また内容に誤りなどありましたら、ご指摘いただけると幸いです!



フラッグシティパートナーズ海外不動産投資セミナー 【DMM FX】入金

Source link