スタディサプリ Product Team Blog

株式会社リクルートが開発するスタディサプリのプロダクトチームのブログです

SwiftUI.NavigationView のバックボタンをカスタマイズする方法

こんにちは。iOS エンジニアの @_nkmrh です。

最近、Full SwiftUI で開発したスタディサプリ 中学講座をリリースし、アプリ開発の醍醐味を味わっています。

はじめに

本稿では、スタディサプリ中学講座 iOS アプリで実装した SwiftUI の NavigationView のバックボタンをカスタマイズする方法について紹介します。

この記事で紹介している方法は iOS 14 以上で動作確認しています。

それでは早速みていきましょう!

カスタムバックボタン

まずはじめに、ナビゲーションバーのバックボタンとして表示したい View を実装します。

ここでは、SFSymbol の chevron.backward を表示しています。

/// ナビゲーションバーのカスタムバックボタン
struct NavigationBarBackButton: View {
  var action: () -> Void

  var body: some View {
    Button(action: action) {
      Image(systemName: "chevron.backward")
        .foregroundColor(.black)
        .frame(width: 36, height: 36, alignment: .leading)
    }
  }
}

このボタンを表示するには、デフォルトのナビゲーションバーバックボタンを非表示にし、toolbar モディファイアを使って ToolbarItem として表示させます。

...
var body: some View {
  NavigationView {
    NavigationLink(isActive: $isActive) {
      Text("Details view")
        .navigationBarBackButtonHidden(true) // ナビゲーションバーバックボタンを非表示
        .toolbar {
          ToolbarItem(placement: .navigationBarLeading) {
            NavigationBarBackButton { // ToolbarItem として表示
              isActive = false
            }
          }
        }
    } label: {
      Text("Show details")
    }
  }
}
...

これでカスタムバックボタンの表示ができました。簡単ですね! さらに、他の View でも再利用しやすいように ViewModifier にしておきましょう。

struct NavigationBarBackButtonViewModifier: ViewModifier {
  var action: () -> Void

  func body(content: Content) -> some View {
    content
      .navigationBarBackButtonHidden(true)
      .toolbar {
        ToolbarItem(placement: .navigationBarLeading) {
          NavigationBarBackButton(action: action)
        }
      }
  }
}

作成した ViewModifier を適用します。

...
NavigationLink(isActive: $isActive) {
  Text("Details view")
    .modifier(
      NavigationBarBackButtonViewModifier { // ViewModifier を適用
        isActive = false
      }
    )
} label: {
  Text("Show details")
}
...

One more thing...

これで完成としたいところですが、この実装のままではエッジスワイプで戻ることができなくなる罠にはまってしまっています。

ユーザービリティを損なってしまうので、改善したいところです。

エッジスワイプ対策

改善するには、先ほど作成した ViewModifier にドラッグジェスチャーを追加して、画面端をスワイプしたら戻るようにします。

...
@GestureState private var dragOffset: CGSize = .zero
private let edgeWidth: CGFloat = 50
private let baseDragWidth: CGFloat = 30
...
.toolbar {
  ToolbarItem(placement: .navigationBarLeading) {
    NavigationBarBackButton(action: action)
  }
}
.highPriorityGesture(
  DragGesture().updating(
    $dragOffset, body: { (value, _, _) in
      if value.startLocation.x < edgeWidth && value.translation.width > baseDragWidth {
        action()
      }
    }
  )
)

どんな View に適用しても問題なく動くように、ここでは highPriorityGesture を使ってエッジスワイプの優先度を上げています。

エッジスワイプの判定はドラッグの開始位置とドラッグされた距離をもとに判定しています。

この方法では、インタラクティブトランジションではないので、デフォルトのエッジスワイプを再現できているわけではないのですが、最低限の利便性を担保するために、このような実装を加えています。

おわりに

SwiftUI での画面遷移は動作が不安定な部分も多く悩まされることも多いように思います。

今後、カスタムバックボタンが必要になった際の参考にしていただければ幸いです。

We’re hiring!

スタディサプリでは、世界の果てまで最高の学びを共に届ける仲間を募集しています。 少しでも気になった方はカジュアル面談もやっていますのでお気軽にお問い合わせください!