見出し画像

[SwiftUI]Tinder風デザイン作ってみる: ジェスチャーによるopacityの状態的変化を見る

こんばんわ、中川(Twitter)です。
今回もこちらを参考にさせていただきます。


参考動画: yusukeさん

非常に丁寧に解説してくださっています。



さて、今回の記事のメインはopacityについてです。

■初めに


opacityとは?

opacityを対象のViewに付与することで、
Viewに不透明度の設定をすることが出来ます。
数値の範囲 ⇨ 0.0(完全透明) ~ 1.0(完全不透明)
例えば、50%の透明度にしたい場合はopacity(0.5) 。

 ドキュメントにはこう書かれています。

Apply opacity to reveal views that are behind another view or to de-emphasize a view.

他のビューの背後にあるビューを明らかにするため、またはビューを強調しないために、opacityを適用します。

公式ドキュメントより


それでは、現状のアプリケーション挙動を見てみます⬇︎

ドラッグ(スワイプ)ジェスチャーによってカードが動き、x軸(横軸)が一定以上にいくとカードが画面外に除外されるという仕様です。
さて、このカードの上部に「GOOD」「NOPE」がありますね?
この文字を、カードをスワイプしてる時だけ出現させたいです。
この仕様をopacityを使って実装していきます。

では、早速実装していきましょう✊

■ 実装



●対象Viewにopacityの付与


まず、状態変数のプロパティを
「GOOD」「NOPE」それぞれの分を作成します。
初期値は0とします。

@State var goodOpacity: Double = 0
@State var nopeOpacity: Double = 0

次に、opacityモディファイアを対象Viewに付与しましょう。
opacityの数値はさっき作った状態変数を引数として当てます。

                                               HStack {
                            Text("GOOD")
                                .font(.system(size: 40, weight: .heavy))
                                .foregroundColor(.green)
                                .padding(.all, 5)
                                .overlay(
                                    RoundedRectangle(cornerRadius: 15)
                                        .stroke(Color.green, lineWidth: 4)
                                )
                                .opacity(goodOpacity)  // ✅

                            Spacer()

                            Text("NOPE")
                                .font(.system(size: 40, weight: .heavy))
                                .foregroundColor(.red)
                                .padding(.all, 5)
                                .overlay(
                                    RoundedRectangle(cornerRadius: 15)
                                        .stroke(Color.red, lineWidth: 4)
                                )
                               .opacity(nopeOpacity)  // ✅
                        }
                        .padding(.all, 30)

「GOOD」「NOPE」それぞれにopacityがセットできました。
この時点で引数の変数値は0のため、二つの文字は完全透明となり画面から姿を消します。


●onChangedを用いてジェスチャーの動きとopacity値の紐付け

次に、この変数の値をスワイプジェスチャーに合わせて状態変化させて、カードを傾けるほどに文字が出現してくる動きを作っていきます。

ジェスチャー(今回はスワイプ)による値の変化を受け取るにはonChangedを扱います。
onChangedについてはカードアニメーションの記事で触れているのでよければ。

⬇︎は、前回作ったonChangedを関数として切り分けたものです。
関数の命名は「dragOnChanged」とします。
引数としてdragGesture.Valueを受け取る必要があります。

// カードViewをドラッグ中の動き
    private func dragOnChanged(value: DragGesture.Value) {

        // translasionプロパティの値にvalue内のtranslation値を参照させて状態変数で変化させている
        self.translation = value.translation

        }
    }

次にこの関数内に、必要なプロパティを三つ作成します。①〜③

// カードViewをドラッグ中の動き
    private func dragOnChanged(value: DragGesture.Value) {

        // translasionプロパティの値にvalue内のtranslation値を参照させて状態変数で変化させている
        self.translation = value.translation

        let diffValue = value.startLocation.x - value.location.x  // ①
        let ratio: CGFloat = 1 / 150  // ②
        let opacity = diffValue * ratio  // ③

        }
    }

それぞれのプロパティの解説をしていきます。

① let diffValue = value.startLocation.x - value.location.x

定数diffValueを宣言。
xは横軸を表します。
value.startLocation.x
(対象Viewの初期位置)から、
value.location.x(対象Viewがスワイプされている時点の位置)を、
引き算した値を格納しています。

② let ratio: CGFloat = 1 / 150

定数ratioを宣言。
CGFloat型で、値は 1 / 150 (150分の1)が格納されています。

③ let opacity = diffValue * ratio

定数opacityを宣言。
先ほど作ったdiffValueratioを掛け算した値が
格納されます。

次に、これらのプロパティを使って条件分岐処理をつけていきます。

●プラス値とマイナス値それぞれの振る舞いを指定

// カードViewをドラッグ中
    private func dragOnChanged(value: DragGesture.Value) {

        // translasionプロパティの値にvalue内のtranslation値を参照させて状態変数で変化させている
        self.translation = value.translation

        let diffValue = value.startLocation.x - value.location.x
        let ratio: CGFloat = 1 / 150
        let opacity = diffValue * ratio

        if value.location.x < value.startLocation.x {  // ✅if文を定義

            self.nopeOpacity = Double(opacity)
            self.goodOpacity = .zero

        } else if value.location.x > value.startLocation.x {

            // opacityのままだと-方向なので、-opasityとすることで+方向にする
            self.goodOpacity = Double(-opacity)
            self.nopeOpacity = .zero

        } // if文 ここまで
    }

こちらのif文分岐は、対象View(カードView)が
プラスの位置の時、マイナスの位置の時で処理を発生させています。

画面の位置情報は、
左に行くほど(-)マイナス、右に行くほど(+)プラスで表されます。


x軸 (横)

[左  ( - )マイナス ⇦⇦⇦[画面中央]⇨⇨⇨ プラス( + )  右]



例えば1行目、

if value.location.x < value.startLocation.x {

この場合、「Viewの初期位置の値よりViewの移動位置の値の方が小さかった場合」です。初期位置の値は0なので、対してカードのスワイプ位置がマイナス方向だった場合ということですね。この条件が当てはまった場合、⬇︎の処理が実行されます。

self.nopeOpacity = Double(opacity)
self.goodOpacity = .zero

最初に宣言した「NOPE」文字用の状態変数nopeOpacityに、
プロパティとして宣言したopacityの値を格納しています。
このnopeOpacityは最終的に透明度を調整する.opacityモディファイアに引数として渡されますが、値はDouble型である必要があるので、型キャストを行っています。
そしてその時「Good」側の値は.zeroにしておくという形ですね。

後述のelse if 以降の記述は、⬆︎とは逆にプラス値の場合の動きを制御します。

} else if value.location.x > value.startLocation.x {

// opacityの値のままだと-方向なので、(-)をつけることで+方向に変換する
            self.goodOpacity = Double(-opacity)
            self.nopeOpacity = .zero
}

基本的な記述は変わりませんね。
注意点が一つ、こちらの制御はプラス値の場合なので、
goodOpacityに渡すopacityの値も( - )を付けてプラス値に変換して渡してあげましょう。

これで、左右それぞれの方向にカードを動かす時に
+方向なら「GOOD」-方向なら「NOPE」が出現します。

+方向なら「GOOD」が出現
ー方向なら「NOPE」が出現

ただこのままだと、スワイプ途中でカードを離した場合(ジェスチャー処理が終了した時)、opacityの値が変化したまま残ってしまいます。
スワイプから指を離してカードが初期位置に戻った時、文字は再び透明化させたいので、ジェスチャー終了時の実行処理を扱うことができるonEndedにコードを足します。

private func dragOnEnded(value: DragGesture.Value) {

        self.goodOpacity = .zero   // ✅
        self.nopeOpacity = .zero   // ✅

        if value.startLocation.x - 150 > value.location.x {

             // 左側にフェードアウト
            self.translation = .init(width: -800, height: 0)

            DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
                self.numbers.removeLast()
                self.translation = .zero
            }


        } else if value.startLocation.x + 150 < value.location.x {

            // 右側にフェードアウト
            self.translation = .init(width: 800, height: 0)
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
                self.numbers.removeLast()
                self.translation = .zero
            }

        } else {
            // 元の位置に戻る
            self.translation = .zero
        }

こちらはonEndedの処理を関数化したdragOnEndedです。
onEndedについては前回のアニメーション記事で書いているので、よければ。
✅部分の二つのコードを足します。
こうすることで、スワイプ処理が終了した時点で値を.zeroにし、文字を完全透明化させます。

スワイプを離すと文字が消える


●文字が出現する対象を最前のカードだけに指定

現状だと後ろのカードにも文字が出現してしまっていますね。
これを最前のカードだけに反映させましょう。

「GOOD」「NOPE」それぞれに付与した.opacityモディファイアの記述に三項演算子を加えます。

Text("GOOD")
  .opacity(self.numbers.last == number ? goodOpacity : .zero)

Text("NOPE")
  .opacity(self.numbers.last == number ? nopeOpacity : .zero)


numbersとは、複数のカード生成で用いたForEach文が参照している配列の値です。⬇︎

@State var numbers = [0,1,2,3,4,5]


ForEach(numbers, id: \.self) { number in

   // カードViewの記述

}

これで、カードの最前列にのみ値の変化が適用されます。

最前列にだけ文字が出現


これでopacity値とジェスチャーアクションの紐付け完成です◎


■まとめ


はい、以上がopacityとジェスチャーの紐付け処理でした。
今回紐付けたのはopacityでしたが、ジェスチャーとある値の変化を連動させる処理は汎用性がありそうですね。
また制作が進んだら記事を書きます。
ではでは。

以上


この記事が気に入ったらサポートをしてみませんか?