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にサンプルのプロジェクトを上げてるのでよかったらどうぞ
参考リンク

