1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【SwiftUI】エッジスワイプを実装してみた

Last updated at Posted at 2023-11-02

NavigationStackの画面遷移では、画面左端を左から右にスワイプすると遷移元に戻ることができます。しかし.navigationBarBackButtonHidden(true) を記述してしまうと戻るボタンが非表示になるだけでなく、スワイプ機能も失われてしまいます。これの対策として今回はエッジスワイプをして遷移元に戻るという機能を実装したいと思います。

標準のエッジスワイプ 今回実装するエッジスワイプ
ezgif.com-video-to-gif (1).gif ezgif.com-video-to-gif (2).gif

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.gestureModifier内で記述することができます。また、valueに対してはさまざま用意されていて、今回は初期位置が取得できる.startLocationと移動量が取得できる.translationを使用しています。ほかに何があるか気になる方はAppleの公式ドキュメントを参考にしてみてください。

取得した数値の比較に必要な値としてedgeWidthdragWidthを用意しています。

private let edgeWidth: CGFloat = 30
private let dragWidth: CGFloat = 30

まず、edgeWidthは左端からどのくらいの横幅をエッジとして定義しているかを示し、スワイプ開始位置であるstartLocationがそのエッジ内にあるかを判定します。
次に、dragWidthはその名の通り、どのくらい画面を横にドラッグしたらスワイプしたという判定になるかを定義していて、実際のスワイプ量であるtranslationと比較して、スワイプしたかどうかを判定しています。

if value.startLocation.x < edgeWidth && value.translation.width > dragWidth

Extensionに追加する

View+DragGesture.swift
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を用意して、それをViewextensionとして追加しています。

実装する際のポイント

extensionで記述した.edgeSwipeを呼び出す際には必ず画面全体でDragGestureを検知できるようにします。

透明なViewを背景にした場合

SecondView.swift
struct SecondView: View {
    
    var body: some View {
        ZStack {
            Color.clear
                .contentShape(Rectangle())
            Text("SecondView")
        }
        .navigationBackButton()
        .edgeSwipe()
    }
}

Color.clearに対しては.contentShapeを付与することでGestureが反応するようになります。透明以外では必要ありません。

VStackを画面全体に広げた場合

SecondView.swift
struct SecondView: View {
    
    var body: some View {
        VStack {
            Text("SecondView")
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .contentShape(Rectangle())
        .navigationBackButton()
        .edgeSwipe()
    }
}

先ほどに比べてframeの指定が増えていますが、 これの方が背景の設定が不要なので余計な記述がなくなります。

ここで取り上げたことをまとめている記事があるのでよかったら参考にしてみてください。

終わりに

githubにサンプルのプロジェクトを上げてるのでよかったらどうぞ

参考リンク

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?