mcpField
S
swiftui-graphics

swiftui-graphics

github.com/athevonデザイン・UI698937 回インストール3 週間前に更新
リポジトリ

swiftui-graphics について

Claude Code、claude.ai、Cowork向けのSwiftUI高度ビジュアルスキルです。Metalシェーダー、Canvas、ビジュアルエフェクト、Liquid Glassを扱い、描画APIの選択や性能、代替手段の指針を提供します。

主な機能

  1. 01Metalシェーダーエフェクト
  2. 02Canvasによる描画とアニメーション
  3. 03ジオメトリに応じたビジュアルエフェクト
  4. 04Liquid Glassの実装パターン
  5. 05描画性能の指針

活用シーン

  1. 01ピクセル単位の色・歪みエフェクトの作成
  2. 02アニメーションするベクター描画
  3. 03スクロールに反応するカード演出
  4. 04対応する画面へのガラス面の追加

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/athevon/genjutsu --skill swiftui-graphics

インストール後、Claude Code で/swiftui-graphicsを入力して読み込まれたか確認します。

詳細

バージョン依存です。 以下のすべての API 名、SDK の対応条件、ブラウザーサポートに関する記述は、 2026-09-08 に一次情報で検証済みです。何をどの情報源で、いつ検証したかは _jutsu/VERSIONS.md をご覧ください。この日付が古い場合は、バージョン番号をもとに行動する前に再確認してください。

SwiftUI Graphics

Metal シェーダー、ビジュアルエフェクト、Liquid Glass、Canvas など、高度な SwiftUI ビジュアルについて説明します。 高度なテーマ(シェーダー、ホログラフィック、リキッドグラス、歪み)で読み込まれます。 基礎については ../swiftui-motion/SKILL.md をご覧ください。 ここでは簡潔なルールを紹介し、詳しい解説は references/ にあります。


判断ツリー:どの API を使うか?

用途API
ピクセル単位の色操作.colorEffect(ShaderLibrary....)
ピクセル位置/歪み.distortionEffect(ShaderLibrary....)
オーバーレイを含むレイヤー全体(シェーダーと背景の合成).layerEffect(ShaderLibrary....)
ジオメトリコンテキストを使うビューモディファイア.visualEffect { content, geometry in }
カスタム描画(パス、グラデーション)Canvas { context, size in }
iOS 26+ のグラスモーフィズム.glassEffect() / GlassEffectContainer
パフォーマンスダンプ.opaque(true) を指定した Canvas の後にエクスポート

段階的に使う API のデフォルト順序: 組み込みモディファイア -> .visualEffect -> Canvas -> Metal シェーダー。ピクセル単位でアニメーションするエフェクトに限り、シェーダーを使ってください。


Metal シェーダーの概要

SwiftUI は iOS 17 に搭載された 3 つのモディファイア .colorEffect、.distortionEffect、.layerEffect を介して、Metal Shading Language(MSL)にバインドします。アプリのターゲットに .metal ファイルを追加し、関数に [[ stitchable ]] 属性を付けると、SwiftUI が ShaderLibrary.<functionName>(...) を使って Swift バインディングを自動生成します。アプリのターゲットごとにライブラリは 1 つです。シェーダーはネイティブ解像度で GPU 上で実行され、引数は Swift から .float、.float2、.color、.image として渡されます。対応状況:iOS 17.0+、iPadOS 17.0+、Mac Catalyst 17.0+、macOS 14.0+、tvOS 17.0+、visionOS 1.0+。watchOS は非対応です。これらより前のバージョンでは、グラデーション、ぼかし、または Canvas にフォールバックしてください。

3 つのスロットは、受け取るデータがそれぞれ異なります。

  • .colorEffect:(position, color) を受け取り、変換後の色を返します。隣接ピクセルのサンプリングはできません。
  • .distortionEffect:(position) を受け取り、新しいサンプル位置を返します。ピクセルは移動しますが、色は変わりません。
  • .layerEffect:(position, SwiftUI::Layer layer) を受け取り、最終的な色を返します。maxSampleOffset の範囲内ならどこでもサンプリングできます。最も負荷が高い方法です。

レシピ:リップル .layerEffect

近くのピクセルを正弦波に沿って変位させる、タッチ時の波紋です。

struct RippleView: View {
    @State var rippleOrigin: CGPoint = .zero
    @State var rippleTime: Float = 0

    var body: some View {
        Image("photo")
            .resizable()
            .scaledToFit()
            .layerEffect(
                ShaderLibrary.ripple(
                    .float2(Float(rippleOrigin.x), Float(rippleOrigin.y)),
                    .float(rippleTime),
                    .float(0.05) // amplitude
                ),
                maxSampleOffset: CGSize(width: 50, height: 50)
            )
            .onTapGesture { location in
                rippleOrigin = location
                rippleTime = 0
                withAnimation(.linear(duration: 1.2)) {
                    rippleTime = 1.2
                }
            }
    }
}
// Ripple.metal
#include <SwiftUI/SwiftUI_Metal.h>
using namespace metal;

[[ stitchable ]]
half4 ripple(float2 position, SwiftUI::Layer layer,
             float2 origin, float time, float amp) {
    float distance = length(position - origin);
    float wave = sin(distance * 0.05 - time * 8.0) * amp;
    float2 dir = normalize(position - origin);
    float falloff = 1.0 / max(distance, 1.0);
    float2 displaced = position + dir * wave * falloff * 50.0;
    return layer.sample(displaced);
}

この方法が機能する理由:

  • [[ stitchable ]] を付けることで、SwiftUI のランタイムからこの関数を利用できるようになります。
  • 最初の 2 つの引数(position、SwiftUI::Layer layer)は、すべての .layerEffect に対して SwiftUI が挿入します。Swift 側の引数はインデックス 2 から始まります。
  • maxSampleOffset は、ビューの境界を越えてどれだけサンプリングできるかを SwiftUI に伝えます。値を小さく見積もるとクリッピングが発生し、大きく見積もると GPU を無駄に使います。

レシピ:ホログラフィック .colorEffect

時間またはスクロールオフセットで動かす、油膜のような虹色のきらめきです。輝度を保つため、暗い領域は暗いままです。

// Holographic.metal
#include <SwiftUI/SwiftUI_Metal.h>
using namespace metal;

[[ stitchable ]]
half4 holographic(float2 position, half4 color, float time) {
    float n = position.x * 0.01 + position.y * 0.005 + time * 0.3;
    half3 rainbow = half3(
        sin(n * 2.0) * 0.5 + 0.5,
        sin(n * 2.0 + 2.094) * 0.5 + 0.5,
        sin(n * 2.0 + 4.188) * 0.5 + 0.5
    );
    half luminance = dot(color.rgb, half3(0.299, 0.587, 0.114));
    return half4(mix(color.rgb, rainbow * luminance * 2.0, 0.5), color.a);
}
struct HolographicCard: View {
    let startTime = Date()

    var body: some View {
        TimelineView(.animation) { timeline in
            let elapsed = Float(timeline.date.timeIntervalSince(startTime))
            Image("card")
                .resizable()
                .scaledToFit()
                .colorEffect(
                    ShaderLibrary.holographic(.float(elapsed))
                )
        }
    }
}

2.094 と 4.188 のオフセットはそれぞれ 2pi/3 と 4pi/3 です。3 つの正弦波の位相を RGB に分散させます。この値は変更しないでください。


レシピ:CRT スキャンライン

スキャンライン、ちらつき、控えめな色収差を加える、ビンテージ CRT エフェクトです。

// CRT.metal
#include <SwiftUI/SwiftUI_Metal.h>
using namespace metal;

[[ stitchable ]]
half4 crt(float2 position, half4 color, float time) {
    float scanline = sin(position.y * 1.5) * 0.04;
    float flicker = sin(time * 60.0) * 0.02;
    half3 result = color.rgb * (1.0 - scanline - flicker);
    // chromatic aberration on R/B channels
    return half4(result.r * 1.05, result.g, result.b * 1.05, color.a);
}
.colorEffect(ShaderLibrary.crt(.float(elapsed)))

R/B のサンプル位置をずらす本格的な色収差には、.layerEffect を使ってください。上記の方法は色合いを加えるだけですが、小さく表示すれば CRT のように見えます。


.visualEffect(iOS 17+)

ビューの GeometryProxy を公開するモディファイアです。GeometryReader の定型コードを使わずに、任意の座標空間におけるビューのフレームに応じて動作を変えられます。

ScrollView {
    LazyVStack(spacing: 16) {
        ForEach(items) { item in
            CardView(item: item)
                .visualEffect { content, proxy in
                    let y = proxy.frame(in: .scrollView).minY
                    let scale = scale(for: y)
                    let opacity = opacity(for: y)
                    return content
                        .scaleEffect(scale)
                        .opacity(opacity)
                }
        }
    }
}

func scale(for y: CGFloat) -> CGFloat {
    let progress = max(0, min(1, y / 600))
    return 0.85 + progress * 0.15
}

用途:

  • 視差カード(スクロールよりゆっくり移動)
  • スクロールに連動した拡大縮小(カードが画面内に入るにつれて拡大)
  • 固定表示されるリビール(.offset(y: max(0, -y)) で位置を制限)

.visualEffect は視覚効果のみを扱います。返されるジオメトリは読み取り専用で、このモディファイアから状態更新をトリガーすることはできません。内部から @State に書き込もうとしないでください。


リキッドグラス(iOS 26)

適応型の奥行きとモーフィングするトランジションを備えた、システム標準のグラスモーフィズムです。OS に組み込まれており、システムレベルで最適化されています。

struct HeroCard: View {
    // @Namespace is a property wrapper - it has to live inside the View, not at file scope.
    @Namespace private var glassNS

    var body: some View {
        if #available(iOS 26.0, macOS 26.0, *) {
            // glassEffectID only does anything inside a GlassEffectContainer.
            GlassEffectContainer(spacing: 20.0) {
                Label("Hero", systemImage: "sparkles")
                    .font(.title2)
                    .padding()
                    // Default shape is a Capsule (DefaultGlassEffectShape); pass `in:` for anything else.
                    .glassEffect(.regular, in: .rect(cornerRadius: 24))
                    .glassEffectID("hero", in: glassNS)
            }
        } else {
            Label("Hero", systemImage: "sparkles")
                .font(.title2)
                .padding()
                .background(.ultraThinMaterial, in: .rect(cornerRadius: 24))
        }
    }
}

ひとまとまりとしてモーフィングさせたいサーフェス(例:ホバー時に個別のピル型に分かれるタブバー)は、コンテナで囲みます。

GlassEffectContainer(spacing: 12) {
    ForEach(tabs) { tab in
        TabIcon(tab: tab)
            .glassEffect(.regular)
            .glassEffectID(tab.id, in: glassNS)
    }
}

iOS 26 より前のバージョンでは、静的なグラスには .background(.ultraThinMaterial) を使用します。モーフィングするトランジションには、マテリアルを背景にしたビューで matchedGeometryEffect を使用してください(swiftui-motion を参照)。

詳細:references/liquid-glass-deep.md。


Canvas(SwiftUI ネイティブ描画)

ベクター描画 API です。SwiftUI を離れることなく、パス、グラデーション、テキスト、ブレンドモードを扱えます。

struct SparkleField: View {
    var body: some View {
        Canvas { context, size in
            for _ in 0..<20 {
                let x = Double.random(in: 0...size.width)
                let y = Double.random(in: 0...size.height)
                let radius = Double.random(in: 1...4)
                context.fill(
                    Path(ellipseIn: CGRect(x: x, y: y, width: radius, height: radius)),
                    with: .color(.white.opacity(.random(in: 0.3...1.0)))
                )
            }
        }
    }
}

アニメーションさせるには、TimelineView(.animation) で囲み、クロージャーがフレームレートで再実行されるようにします。

TimelineView(.animation) { timeline in
    Canvas { context, size in
        let t = timeline.date.timeIntervalSinceReferenceDate
        // draw using t as time
    }
}

TimelineView(.animation) は画面のリフレッシュレートで再描画します。低速なアニメーションで消費電力を抑えるには、.animation(minimumInterval: 0.1) を使用します。

詳細:references/canvas-swiftui.md。


パフォーマンスに関する考慮事項

エフェクトコスト備考
.colorEffect低ピクセルごとに実行されます。単純な計算で、隣接ピクセルにはアクセスしません
.distortionEffect中サンプリングのコストがかかります。分岐をなくすことが重要です
.layerEffect高レイヤー全体にアクセスでき、任意の場所をサンプリングできます
Canvas と TimelineView変動描画回数と複雑さによって異なります
.glassEffect中〜高GPU 負荷が高いですが、最新のデバイスでは問題ありません

ルール:

  1. 同じビューに .layerEffect を 1 つより多く重ねないでください。それぞれが完全なレンダーパスになります。
  2. 静的な部分(グラデーションやパス)は、アニメーション対象のサブツリーの外で事前計算します。
  3. むやみに最適化する前に、Instruments の GPU Frame Capture と Metal System Trace でプロファイルしてください。

アンチパターン

1. .colorEffect モディファイアを重ねる

各モディファイアが個別のレンダーパスになります。

// BAD -- 5 GPU passes
Image(...)
    .colorEffect(ShaderLibrary.tint(...))
    .colorEffect(ShaderLibrary.scanlines(...))
    .colorEffect(ShaderLibrary.grain(...))
    .colorEffect(ShaderLibrary.vignette(...))
    .colorEffect(ShaderLibrary.chromatic(...))

// GOOD -- one shader does all the ops in one pass
Image(...)
    .colorEffect(ShaderLibrary.crtCombo(.float(time)))
2. 状態が変わるたびに Canvas を再描画する

祖先ビューの状態が変わるたびに Canvas が再実行されます。TimelineView や EquatableView を使わないと、想定していない入力でも再描画されます。

// BAD -- redraws on parent re-render
struct Parent: View {
    @State var unrelated = 0
    var body: some View {
        VStack {
            Button("tick") { unrelated += 1 }
            Canvas { context, size in expensiveDraw(context, size) }
        }
    }
}

// GOOD -- isolate via EquatableView or TimelineView
TimelineView(.animation) { _ in
    Canvas { context, size in expensiveDraw(context, size) }
}
3. シェーダー内に色の値をハードコードする

色を変更するたびに再コンパイルが必要になります。値は引数として渡してください。

// BAD
half3 tint = half3(1.0, 0.4, 0.2);

// GOOD -- accept color from Swift
[[ stitchable ]]
half4 tinted(float2 pos, half4 color, half4 tint) {
    return half4(color.rgb * tint.rgb, color.a);
}
.colorEffect(ShaderLibrary.tinted(.color(themeAccent)))
4. .glassEffect をあらゆる場所で使う

リキッドグラスはコストが高く、使いすぎると視覚的なノイズになります。ヒーローやクロームのサーフェスに限定してください。

// BAD -- 30 glass cards in a list
LazyVStack {
    ForEach(items) { item in
        Card(item: item).glassEffect(.regular) // GPU melt
    }
}

// GOOD -- glass on the floating tab bar, opaque cards underneath
ZStack(alignment: .bottom) {
    ScrollView { LazyVStack { ForEach(items) { Card(item: $0) } } }
    TabBar().glassEffect(.regular)
}

クイックリファレンス

用途読み込む項目
Metal レシピの詳細references/metal-recipes.md
リキッドグラスのパターンと iOS 26 固有の情報references/liquid-glass-deep.md
Canvas の描画パターンreferences/canvas-swiftui.md
基本的なアニメーション../swiftui-motion/SKILL.md
基礎../motion-principles/SKILL.md
モバイル UX(iOS)../mobile-principles/SKILL.md
デスクトップ UX(macOS)../desktop-principles/SKILL.md

ソース

コメント