NavigationStackの画面遷移では、画面左端を左から右にスワイプすると遷移元に戻ることができます。しかし.navigationBarBackButtonHidden(true)
を記述してしまうと戻るボタンが非表示になるだけでなく、スワイプ機能も失われてしまいます。これの対策として今回はエッジスワイプをして遷移元に戻るという機能を実装したいと思います。
標準のエッジスワイプ | 今回実装するエッジスワイプ |
---|---|
NavigationStack標準のエッジスワイプでは、画面自体を指で左右に動的に動かすことができますが、今回実装するのは画面上でスワイプをしたら自動で画面遷移するというものになります。そのため、標準のものとはUXが異なります。
NavigationStackの戻るボタンのカスタマイズをした上で追加の機能を実装するということでエッジスワイプについて説明するので、全体のソースコードが知りたいという方はこちらの記事をご覧ください。
DragGestureを使って実装してみる
DragGestureとは
画面に触れている位置(座標)を取得して、その値をもとにアクションを実行するためのコンポーネントです。
onChanged
たった今画面に触れている位置(座標)を取得して、value
として渡してもらえます。.updating
との大きな違いは、画面から指を離した場所をvalue
が保持し続けるという点です。
DragGesture().onChanged { value in
}
updating
onChanged
と同様に触れている位置を取得しますが、@GestureState
というプロパティラッパーを使用して値を常時更新します。value
で値を取得し、state
で管理し、transaction
で値をリセットするといったイメージです。
@GestureState private var dragOffset: CGSize = .zero
...
.updating($dragOffset, body: { (value, state, transaction) in
state = value.translation
})
今回はシンプルに書けるonChanged
を使用して実装したいと思います。
実際にDragGesture
の部分だけ書いてみると次のようになります。
.gesture (
DragGesture().onChanged { value in
if value.startLocation.x < edgeWidth && value.translation.width > dragWidth {
dismiss()
}
}
)
先ほどのDragGesture().onChanged
は.gesture
のModifier
内で記述することができます。また、valueに対してはさまざま用意されていて、今回は初期位置が取得できる.startLocation
と移動量が取得できる.translation
を使用しています。ほかに何があるか気になる方はAppleの公式ドキュメントを参考にしてみてください。
取得した数値の比較に必要な値としてedgeWidth
とdragWidth
を用意しています。
private let edgeWidth: CGFloat = 30
private let dragWidth: CGFloat = 30
まず、edgeWidth
は左端からどのくらいの横幅をエッジとして定義しているかを示し、スワイプ開始位置であるstartLocation
がそのエッジ内にあるかを判定します。
次に、dragWidth
はその名の通り、どのくらい画面を横にドラッグしたらスワイプしたという判定になるかを定義していて、実際のスワイプ量であるtranslation
と比較して、スワイプしたかどうかを判定しています。
if value.startLocation.x < edgeWidth && value.translation.width > dragWidth
Extensionに追加する
struct EdgeSwipe: ViewModifier {
@Environment(\.dismiss) var dismiss
private let edgeWidth: Double = 30
private let baseDragWidth: Double = 30
func body(content: Content) -> some View {
content
.gesture (
DragGesture().onChanged { value in
if value.startLocation.x < edgeWidth && value.translation.width > baseDragWidth {
dismiss()
}
}
)
}
}
extension View {
func edgeSwipe() -> some View {
self.modifier(EdgeSwipe())
}
}
ViewModifier
を用意して、それをView
のextension
として追加しています。
実装する際のポイント
extension
で記述した.edgeSwipe
を呼び出す際には必ず画面全体でDragGesture
を検知できるようにします。
透明なViewを背景にした場合
struct SecondView: View {
var body: some View {
ZStack {
Color.clear
.contentShape(Rectangle())
Text("SecondView")
}
.navigationBackButton()
.edgeSwipe()
}
}
Color.clear
に対しては.contentShape
を付与することでGesture
が反応するようになります。透明以外では必要ありません。
VStackを画面全体に広げた場合
struct SecondView: View {
var body: some View {
VStack {
Text("SecondView")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
.navigationBackButton()
.edgeSwipe()
}
}
先ほどに比べてframe
の指定が増えていますが、 これの方が背景の設定が不要なので余計な記述がなくなります。
ここで取り上げたことをまとめている記事があるのでよかったら参考にしてみてください。
終わりに
githubにサンプルのプロジェクトを上げてるのでよかったらどうぞ
参考リンク