🌈

LinearGradientで虹色グラデーションを実装してUnitPointを魔法のように扱う

2025/02/21に公開

はじめに

こんにちは!こんばんは!
最近夜のサウナの外気浴が少し寒いと感じているdelyのtakkyです!
今回は、横長のボタンに対して斜めのレインボーグラデーションを実装する機会があり、実装後改めて整理するために作成したUnitPointのstartPointとendPointの組み合わせによる見た目の変化の検証結果アウトプットを紹介します!
グラデーションの実装は、SwiftUIのLinearGradientを用いて行い、グラデーションの模様指定に関しては、UnitPointを指定して、正方形と長方形の図形に対して、グラデーションをつけて可視化しました。

検証で行ったこと

具体的に今回プロダクトで利用するために実装したUIは、以下のレインボーグラデーションで個々の色は左下から右上に30度ぐらい斜めに流れるようなものです!(プロジェクトではもう少し凝っている色を使っているので、今回題材にしているのは、単純なSwiftUI.Colorの色を利用して虹色のボタンを作成しています。)

rainbow

上記の実装では、LinearGradientという線形のグラデーションを作成するための構造体を利用しました。(公式のドキュメントはこちら
細かいグラデーション模様の設定を決めるには、まず虹色を構成する色を赤から紫まで順番に配列で渡して、グラデーション開始位置は、UnitPointでstartPointendPointを指定してUIを実装しました。
具体的にどのようなグラデーションのUIができるのかは、以下にある『正方形の虹色グラデーションUIの場合』と『長方形(横長)の虹色グラデーションUIの場合』で確認できます。

正方形の虹色グラデーションUIの場合

横軸がstartPointにUnitPoint、縦軸にendPointの指定を行った表で、それぞれ左上から.leading, .trailing, .center, .top, .bottomを組み合わせてパターン表を作成して検証を行いました。

leading trailing center top bottom
leading leading leading leading trailing leading center leading top leading bottom
trailing trailing leading trailing trailing trailing center trailing top trailing bottom
center center leading center trailing center center center top center bottom
top top leading top trailing top center top top top bottom
bottom bottom leading bottom trailing bottom center bottom top bottom bottom
正方形の虹色グラデーションUIサンプルコード
struct RainbowLinearGradient: View {
    /// UnitPoint のStartPointとEndPointの組み合わせを網羅する為の型
    struct GradientConfiguration: Identifiable {
        let id = UUID()
        let startPoint: UnitPoint
        let endPoint: UnitPoint
    }

    let allGradientCombinations: [GradientConfiguration] = [
        GradientConfiguration(startPoint: .leading, endPoint: .leading),
        GradientConfiguration(startPoint: .leading, endPoint: .trailing),
        GradientConfiguration(startPoint: .leading, endPoint: .center),
        GradientConfiguration(startPoint: .leading, endPoint: .top),
        GradientConfiguration(startPoint: .leading, endPoint: .bottom),
        GradientConfiguration(startPoint: .trailing, endPoint: .leading),
        GradientConfiguration(startPoint: .trailing, endPoint: .trailing),
        GradientConfiguration(startPoint: .trailing, endPoint: .center),
        GradientConfiguration(startPoint: .trailing, endPoint: .top),
        GradientConfiguration(startPoint: .trailing, endPoint: .bottom),
        GradientConfiguration(startPoint: .center, endPoint: .leading),
        GradientConfiguration(startPoint: .center, endPoint: .trailing),
        GradientConfiguration(startPoint: .center, endPoint: .center),
        GradientConfiguration(startPoint: .center, endPoint: .top),
        GradientConfiguration(startPoint: .center, endPoint: .bottom),
        GradientConfiguration(startPoint: .top, endPoint: .leading),
        GradientConfiguration(startPoint: .top, endPoint: .trailing),
        GradientConfiguration(startPoint: .top, endPoint: .center),
        GradientConfiguration(startPoint: .top, endPoint: .top),
        GradientConfiguration(startPoint: .top, endPoint: .bottom),
        GradientConfiguration(startPoint: .bottom, endPoint: .leading),
        GradientConfiguration(startPoint: .bottom, endPoint: .trailing),
        GradientConfiguration(startPoint: .bottom, endPoint: .center),
        GradientConfiguration(startPoint: .bottom, endPoint: .top),
        GradientConfiguration(startPoint: .bottom, endPoint: .bottom)
    ]

    var body: some View {
        ScrollView {
            ForEach(allGradientCombinations) { unitPoints in
                VStack(alignment: .center, spacing: .zero) {
                    LinearGradient(
                        gradient: Gradient(
                            colors: [
                                Color.red,
                                Color.orange,
                                Color.yellow,
                                Color.green,
                                Color.blue,
                                Color.indigo,
                                Color.purple
                            ]
                        ),
                        startPoint: unitPoints.startPoint,
                        endPoint: unitPoints.endPoint
                    )
                    .aspectRatio(1, contentMode: .fit)
                    .cornerRadius(8)
                    .overlay(gradientGuideOverlay(), alignment: .center) // 座標ガイドを追加
                    // 座標情報の表示
                    Text("Start: \(unitPoints.startPoint) → End: \(unitPoints.endPoint)")
                        .font(.subheadline)
                        .padding(.top, 4)
                }
            }
            .padding(.horizontal, 16)
        }
    }
    /// グラデーションの座標系をわかりやすくするためのガイド
    @ViewBuilder
    private func gradientGuideOverlay() -> some View {
        GeometryReader { proxy in
            let size = proxy.size

            Path { path in
                // 水平方向のガイドライン
                path.move(to: CGPoint(x: 0, y: size.height / 2))
                path.addLine(to: CGPoint(x: size.width, y: size.height / 2))

                // 垂直方向のガイドライン
                path.move(to: CGPoint(x: size.width / 2, y: 0))
                path.addLine(to: CGPoint(x: size.width / 2, y: size.height))
            }
            .stroke(Color.white.opacity(0.5), lineWidth: 1)

            // 各ポイントの座標を表示
            VStack {
                HStack {
                    Text("(0, 0)").foregroundColor(.white).font(.caption)
                    Spacer()
                    Text("(0.5, 0)").foregroundColor(.white).font(.caption)
                    Spacer()
                    Text("(1, 0)").foregroundColor(.white).font(.caption)
                }
                Spacer()
                HStack {
                    Text("(0, 0.5)").foregroundColor(.white).font(.caption)
                    Spacer()
                    Text("(0.5, 0.5)").foregroundColor(.white).font(.caption)
                    Spacer()
                    Text("(1, 0.5)").foregroundColor(.white).font(.caption)
                }
                Spacer()
                HStack {
                    Text("(0, 1)").foregroundColor(.white).font(.caption)
                    Spacer()
                    Text("(0.5, 1)").foregroundColor(.white).font(.caption)
                    Spacer()
                    Text("(1, 1)").foregroundColor(.white).font(.caption)
                }
            }
            .padding(4)
        }
    }
}

正方形や正方形に限りなく近いUIへのUnitPointの指定であれば、かなり簡単にある程度直感的に実装することができ、グラデーションの調整が楽であることがわかりました!
startPointとendPointの指定が同じ場合であれば、Gradientcolorsの配列に指定している最後の色がUIに反映されるのも納得ですね👍

長方形(横長)の虹色グラデーションUIの場合

次に、今回私が実装したかった、長方形のUIにグラデーションをつけてみたパターンの検証です!
こちらも同じくstartPointとendPointにそれぞれ.leading〜.bottomを組み合わせてパターン表を作成してみました。

leading trailing center top bottom
leading leading leading leading trailing leading center leading top leading bottom
trailing trailing leading trailing trailing trailing center trailing top trailing bottom
center center leading center trailing center center center top center bottom
top top leading top trailing top center top top top bottom
bottom bottom leading bottom trailing bottom center bottom top bottom bottom
長方形(横長)の虹色グラデーションUIサンプルコード
struct RainbowLinearGradient: View {
    /// UnitPoint のStartPointとEndPointの組み合わせを網羅する為の型
    struct GradientConfiguration: Identifiable {
        let id = UUID()
        let startPoint: UnitPoint
        let endPoint: UnitPoint
    }

    let allGradientCombinations: [GradientConfiguration] = [
        GradientConfiguration(startPoint: .leading, endPoint: .leading),
        GradientConfiguration(startPoint: .leading, endPoint: .trailing),
        .
        .
        .
        GradientConfiguration(startPoint: .bottom, endPoint: .top),
        GradientConfiguration(startPoint: .bottom, endPoint: .bottom)
    ]

    var body: some View {
        ScrollView {
            ForEach(allGradientCombinations) { unitPoints in
                VStack {
                    LinearGradient(
                        gradient: Gradient(
                            colors: [
                                Color.red,
                                Color.orange,
                                Color.yellow,
                                Color.green,
                                Color.blue,
                                Color.indigo,
                                Color.purple
                            ]
                        ),
                        startPoint: unitPoints.startPoint,
                        endPoint: unitPoints.endPoint
                    )
                    .frame(maxWidth: .infinity, maxHeight: 56)
                    .aspectRatio(1, contentMode: .fit)
                    .clipShape(.capsule)
                    // 座標情報の表示
                    Text("Start: \(unitPoints.startPoint) → End: \(unitPoints.endPoint)")
                        .font(.subheadline)
                        .padding(.top, 4)
                }
            }
            .padding(.horizontal, 16)
        }
    }
}

こうみてみると正方形のUIに対して虹色のグラデーションを左下から右上に表示するには、デフォルトの.leading, .trailing, .center, .top, .bottomだけでは細かい色の斜めのグラデーションを作成はできないみたいですね🤔

この理由としては、UnitPointは相対的な位置を定義するものなので、正方形と長方形の図形を比較すると、同じようなグラデーションにならないということですね!

なので、私はプロダクトで利用するUIに関しては、Previewをフル活用して思い通りのUIを完成させられるように、UnitPointのinit(x: CGFloat, y: CGFloat)を活用をして(x軸とy軸に数値を指定して)少しずつ近づけて実装完了させました!!

repost
再掲

今回実装で必要になったグラデーションに近いサンプルコード
struct RainbowLinearGradient: View {
    var body: some View {
        LinearGradient(
            gradient: Gradient(
                colors: [
                    Color.red,
                    Color.orange,
                    Color.yellow,
                    Color.green,
                    Color.blue,
                    Color.indigo,
                    Color.purple
                ]
            ),
            // 虹色のグラデーションを左下から右上に並べるために細かい指定が必要
            startPoint: .init(x: 0.7, y: -1.8),
            endPoint: .init(x: 0.9, y: 0.8)
        )
        .frame(maxWidth: .infinity, maxHeight: 56)
        .aspectRatio(1, contentMode: .fit)
        .clipShape(Capsule())
        .padding(.horizontal, 16)
    }
}

これでやっといい感じに左下から右上に対しての虹色グラデーションを作成できました!🙌
何事も細かいUIを実装するには、デフォルト値で実装するよりかは、微調整しながら任意の値をいれて試すのがいいですね!

おわりに

UnitPointのstartPointとendPointに値を指定すると簡単にグラデーションをつけることができるので、サクッとリッチなUIを作成するには便利ですね!
また、init(x: CGFloat, y: CGFloat)のように細かい位置を設定して斜線を急にしたりしてUIを逐一確認するには、Previewを活用しないとかなり効率悪いので、Previewのありがたみを改めて感じました!
また、サクッと検証した結果だけアウトプットテックブログも別の実装で生まれたらシェアしたいと思います!

参考記事

https://developer.apple.com/documentation/swiftui/lineargradient

https://developer.apple.com/documentation/swiftui/unitpoint

https://developer.apple.com/documentation/swiftui/unitpoint/init(x:y:)

dely Tech Blog

Discussion