![見出し画像](https://assets.st-note.com/production/uploads/images/80899361/rectangle_large_type_2_cb4205815207bb59cd5c38a05ef3d52e.png?width=1200)
[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を適用します。
それでは、現状のアプリケーション挙動を見てみます⬇︎
![](https://assets.st-note.com/production/uploads/images/80896383/picture_pc_8ddcbf9239ae8bd4ad4caa496943b51f.gif)
ドラッグ(スワイプ)ジェスチャーによってカードが動き、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のため、二つの文字は完全透明となり画面から姿を消します。
![](https://assets.st-note.com/img/1655487429628-DkeZ509mes.png)
●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を宣言。
先ほど作ったdiffValueとratioを掛け算した値が
格納されます。
次に、これらのプロパティを使って条件分岐処理をつけていきます。
●プラス値とマイナス値それぞれの振る舞いを指定
// カード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」が出現します。
![](https://assets.st-note.com/production/uploads/images/80898678/picture_pc_53a944e90e1f4d680b510a9bda4bc7cd.gif)
![](https://assets.st-note.com/production/uploads/images/80898684/picture_pc_440a82aea58f2f45daa46f45472020b6.gif)
ただこのままだと、スワイプ途中でカードを離した場合(ジェスチャー処理が終了した時)、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にし、文字を完全透明化させます。
![](https://assets.st-note.com/production/uploads/images/80898903/picture_pc_5b9387378a6cabab0244ada1422391b2.gif)
●文字が出現する対象を最前のカードだけに指定
現状だと後ろのカードにも文字が出現してしまっていますね。
これを最前のカードだけに反映させましょう。
「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の記述
}
これで、カードの最前列にのみ値の変化が適用されます。
![](https://assets.st-note.com/production/uploads/images/80899114/picture_pc_93f9e9f612a0de8627b53b9cee213055.gif)
これでopacity値とジェスチャーアクションの紐付け完成です◎
■まとめ
はい、以上がopacityとジェスチャーの紐付け処理でした。
今回紐付けたのはopacityでしたが、ジェスチャーとある値の変化を連動させる処理は汎用性がありそうですね。
また制作が進んだら記事を書きます。
ではでは。
以上