SwiftUIの花びら描画の落とし穴:シェイププロトコルの実践

SwiftUI Shape Protocolを使用してカスタムの花びらの形状を描画する完全なプロセスの詳細な記録。addQuadCurveや座標系などの実用的なヒントも含まれています。

きっかけ

最近、以前作ったWeChatミニプログラム「花びら単語チャレンジ」をiOSに移植したいと思いました。このミニプログラムは花びら上の文字をタップして単語を作るゲームで、CSSで花びらを描くのは簡単でしたが、SwiftUIに変えると頭が痛くなりました。

SwiftUIは半人前の私(これまで技術的に大したことないアプリをいくつか作っただけ)にとって、花びらを描くことすらどこから手をつければいいかわかりませんでした。「SwiftUI 花びら」でググってこの記事を見つけました。理想の形ではなかったですが、少なくとも糸口にはなりました。

Shapeプロトコルとは?

簡単に言うと、SwiftUIのShapeプロトコルは自分で形状を描くためのツールです。path(in rect: CGRect) -> Pathメソッドを実装し、描き方を指示するだけです。

最初の試み:addArcで弧を描く

参考記事ではaddArcメソッドを使っていました:

struct DaisyPental: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        path.move(to: CGPoint(x: rect.minX, y: rect.midY))
        path.addArc(
            center: CGPoint(x: rect.maxX, y: rect.midY), 
            radius: rect.maxX*(1/11),
            startAngle: .degrees(90),
            endAngle: .degrees(270),
            clockwise: true
        )
        return path
    }
}

このコードの動作を理解するため、Xcodeで試しに赤い背景を追加してみました:

struct ShapTest: View {
    var body: some View {
        DaisyPental()
            .fill(.yellow.gradient)
            .frame(width: 110, height: 20)
            .background(.red)  // 赤背景で可視化
    }
}

SwiftUIの座標系は直感に反する

ここに落とし穴があります。SwiftUIの座標系は一般的な考え方と異なります:

  • 原点が左上:左下ではありません
  • X軸:右方向が正(これは普通)
  • Y軸:下方向が正(これは直感に反します)

つまり:

  • rect.minX, rect.minY → 左上隅
  • rect.maxX, rect.maxY → 右下隅
  • rect.midX, rect.midY → 中心点

XcodeのCanvasプレビューを開きながらpath.moveのx、y値を変更して効果を確認できます。

addArcの角度定義はさらに直感に反する

addArcの角度定義は特に独特です:

  • 0° → 右側
  • 90° → 下側(上ではない!)
  • 180° → 左側
  • 270° → 上側

数学で使う座標系と完全に異なり、長時間ハマりました。

addQuadCurve:曲線描画の神器

後でaddArcでは理想の花びら形状が描けないと気づき、YouTubeでaddQuadCurveを使っている例を見て試したところ、こちらが遥かに使いやすかったです。

addQuadCurveの原理はシンプルです:

path.addQuadCurve(
    to: CGPoint,      // 終点
    control: CGPoint  // 制御点
)
  • 始点:ペンの現在位置
  • 終点:曲線の終了位置
  • 制御点:曲線の曲がり方を決定するポイント

数学的詳細は完全には理解していませんが、制御点を調整するだけで曲線が変化します。

完成した花びら実装

様々な試行錯誤の末、この花びら形状に到達しました:

import SwiftUI

struct PetalShape: Shape {
    func path(in rect: CGRect) -> Path {
        Path { path in
            // 描画出発点
            path.move(to: CGPoint(x: rect.minX + 30, y: rect.minY + 30))
            
            // 花びらの左側曲線
            path.addQuadCurve(
                to: CGPoint(x: rect.minX + 30, y: rect.minY - 30),
                control: CGPoint(x: rect.minX - 10, y: rect.minY)
            )
            
            // 花びらの先端部分
            path.addQuadCurve(
                to: CGPoint(x: rect.maxX * 1.8, y: rect.minY),
                control: CGPoint(x: rect.midX + 40, y: -rect.maxY + 30)
            )
            
            // 花びらの右側(開始点に戻る)
            path.addQuadCurve(
                to: CGPoint(x: rect.minX + 30, y: rect.minY + 30),
                control: CGPoint(x: rect.midX + 40, y: rect.maxY - 30)
            )
        }
    }
}

使用例:

struct ShapeTest: View {
    var body: some View {
        PetalShape()
            .fill(.yellow.gradient)
            .frame(width: 110, height: 110)
    }
}

次のステップ:花の完成形

1枚の花びらができれば、花全体の構成は簡単です:

  1. 複数の花びらを複製
  2. rotationEffectで回転
  3. offsetで位置調整
  4. 組み合わせて完成の花に

ハマった経験からの学び

このプロセスで得た主な気づき:

  • SwiftUI座標系:Y軸下方と角度定義は特に直感に反する
  • addQuadCurveの優位性:自然な曲線描画ではaddArcより遥かに優れる
  • 制御点の重要性:位置調整こそが理想の形状への鍵
  • Canvasプレビューの活用:コード変更を即時確認が必須

まとめ

紆余曲折ありましたが、無事に花びら形状を実装できました。「花びら単語チャレンジ」iOS版の開発を継続し、早期リリースを目指します。

この記事は主に問題解決のプロセスを記録するもので、完全に明確ではないかもしれませんが、記録する意志自体が進歩だと考えています。

SwiftUIカスタムシェイプに取り組んでいる方の参考になれば幸いです。質問があればぜひ議論しましょう。

参考資料