100

I want to add a custom navigation button that will look somewhat like this:

desired navigation back button

Now, I've written a custom BackButton view for this. When applying that view as leading navigation bar item, by doing:

.navigationBarItems(leading: BackButton())

...the navigation view looks like this:

current navigation back button

I've played around with modifiers like:

.navigationBarItem(title: Text(""), titleDisplayMode: .automatic, hidesBackButton: true)

without any luck.

Question

How can I...

  1. set a view used as custom back button in the navigation bar? OR:
  2. programmatically pop the view back to its parent?
    When going for this approach, I could hide the navigation bar altogether using .navigationBarHidden(true)
1

15 Answers 15

163

TL;DR

Use this to transition to your view:

NavigationLink(destination: SampleDetails()) {}

Add this to the view itself:

@Environment(\.dismiss) private var dismiss

Then, in a button action or something, dismiss the view:

dismiss()

Full code

From a parent, navigate using NavigationLink

 NavigationLink(destination: SampleDetails()) {}

In DetailsView hide navigationBarBackButton and set custom back button to leading navigationBarItem,

struct SampleDetails: View {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

    var btnBack : some View { Button(action: {
        self.presentationMode.wrappedValue.dismiss()
        }) {
            HStack {
            Image("ic_back") // set image here
                .aspectRatio(contentMode: .fit)
                .foregroundColor(.white)
                Text("Go back")
            }
        }
    }
    
    var body: some View {
            List {
                Text("sample code")
        }
        .navigationBarBackButtonHidden(true)
        .navigationBarItems(leading: btnBack)
    }
}
Sign up to request clarification or add additional context in comments.

11 Comments

this works great except presentationMode.value is now presentationMode.wrappedValue, however, this seems to disable the default swipe to go back behavior. any idea on how to enable it again?
any idea on how to add the swipe back?
Thanks , work fine, but we have to take care with the image size. Always remember .resizable() method for custom frame size.
This solution has been deprecated for iOS 15. Use DismissAction now stackoverflow.com/a/72704145/4975772
|
37

SwiftUI - iOS 17

It looks like you can now combine the navigationBarBackButtonHidden and .toolbar to get the effect you're trying to achieve.

Code

struct Navigation_CustomBackButton: View {
    var body: some View {
        NavigationStack {
            NavigationLink("Go To Detail",
                           destination: Navigation_CustomBackButton_Detail())
            .font(.title)
            .navigationTitle("Navigation Views")
        }
    }
}
// Second Screen
struct Navigation_CustomBackButton_Detail: View {
    @Environment(\.dismiss) var dismiss
    
    var body: some View {
        VStack {
        }
        .navigationTitle("Detail View")
        .navigationBarBackButtonHidden(true)
        .toolbar {
            ToolbarItem(placement: .topBarLeading) {
                Button(action: {
                    dismiss()
                }) {
                    Label("Back", systemImage: "arrow.left.circle")
                }
            }
        }
    }
}

Example

Here is what it looks like (excerpt from the "SwiftUI Views Mastery" book): Custom Back Button in SwiftUI

👉 Note the warning on that page: You will lose swipe-back functionality if that is important to your users.

2 Comments

This disables swipe to go back.
Also this doesn't work in my case when I'm using a UIKitish navigation controller, with contained SwiftUI views as vcs. In one such subview I need to hide the nav bar completely, but still implement the back button in SwiftUI and still I want to keep the swipe-to-go-back feature functioning.
31

iOS 15+

presentationMode.wrappedValue.dismiss() is now deprecated.

It's replaced by DismissAction

private struct SheetContents: View {
    @Environment(\.dismiss) private var dismiss

    var body: some View {
        Button("Done") {
            dismiss()
        }
    }
}

You can create a custom back button that will use this dismiss action

struct CustomBackButton: View {
    let dismiss: DismissAction
    
    var body: some View {
        Button {
            dismiss()
        } label: {
            Image("...custom back button here")
        }
    }
}

then attach it to your view.

.navigationBarBackButtonHidden(true) // Hide default button
.navigationBarItems(leading: CustomBackButton(dismiss: self.dismiss)) // Attach custom button

6 Comments

could you explain the correlation between the two block of codes?
@JavierHeisecke The top code block is just a standalone example of how to dismiss. The bottom two show how to add a custom back button that will dismiss when tapped.
Straight to point. Correct answer!
@joshuakcockrell Doesn't work anymore :/
@Nikola.Lukovic What do you mean? We're actively using this in production with several hundred users. Check your iOS version. Are you in a NavigationView/NavigationStack? Make a minimal example. Comment out everything in your entire app besides the main WindowGroup and put this in to see if it's still broken.
|
14

You can use UIAppearance for this:

if let image = UIImage(named: "back-button") {
    UINavigationBar.appearance().backIndicatorImage = image
    UINavigationBar.appearance().backIndicatorTransitionMaskImage = image
}

This should be added early on in your app like App.init. This also preserves the native swipe back functionality.

Comments

11

Based on other answers here, this is a simplified answer for Option 2 working for me in XCode 11.0:

struct DetailView: View {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

    var body: some View {

        Button(action: {
           self.presentationMode.wrappedValue.dismiss()
        }) {
            Image(systemName: "gobackward").padding()
        }
        .navigationBarHidden(true)

    }
}

Note: To get the NavigationBar to be hidden, I also needed to set and then hide the NavigationBar in ContentView.

struct ContentView: View {
    var body: some View {
        NavigationView {
            VStack {
                NavigationLink(destination: DetailView()) {
                    Text("Link").padding()
                }
            } // Main VStack
            .navigationBarTitle("Home")
            .navigationBarHidden(true)

        } //NavigationView
    }
}

Comments

8

Here's a more condensed version using principles shown in the other comments to change only the text of the button. The chevron.left icon can also be easily replaced with another icon.

Create your own button, then assign it using .navigationBarItems(). I found the following format most nearly approximated the default back button.

    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

    var backButton : some View {
        Button(action: {
            self.presentationMode.wrappedValue.dismiss()
        }) {
            HStack(spacing: 0) {
                Image(systemName: "chevron.left")
                    .font(.title2)
                Text("Cancel")
            }
        }
    }

Make sure you use .navigationBarBackButtonHidden(true) to hide the default button and replace it with your own!

        List(series, id:\.self, selection: $selection) { series in
            Text(series.SeriesLabel)
        }
        .navigationBarBackButtonHidden(true)
        .navigationBarItems(leading: backButton)

1 Comment

This works well but is there a better way so that backButton can be shared over several views?
6

I expect you want to use custom back button in all navigable screens, so I wrote custom wrapper based on @Ashish answer.

struct NavigationItemContainer<Content>: View where Content: View {
    private let content: () -> Content
    @Environment(\.presentationMode) var presentationMode

    private var btnBack : some View { Button(action: {
        self.presentationMode.wrappedValue.dismiss()
    }) {
        HStack {
            Image("back_icon") // set image here
                .aspectRatio(contentMode: .fit)
                .foregroundColor(.black)
            Text("Go back")
        }
        }
    }

    var body: some View {
        content()
            .navigationBarBackButtonHidden(true)
            .navigationBarItems(leading: btnBack)
    }

    init(@ViewBuilder content: @escaping () -> Content) {
        self.content = content
    }
}

Wrap screen content in NavigationItemContainer:

Usage:

struct CreateAccountScreenView: View {
    var body: some View {
        NavigationItemContainer {
            VStack(spacing: 21) {
                AppLogoView()
                //...
            }
        }
    }
}

1 Comment

There's a problem in your CreateAccountScreenView. If you have another navigation trailing items within CreateAccountScreenView, then only trailing or leading navigation items. This is because you can not define navigationBarItems twice and only one works
6

Swiping is not disabled this way.

Works for me. XCode 11.3.1

Put this in your root View

init() {
    UINavigationBar.appearance().isUserInteractionEnabled = false
    UINavigationBar.appearance().backgroundColor = .clear
    UINavigationBar.appearance().barTintColor = .clear
    UINavigationBar.appearance().setBackgroundImage(UIImage(), for: .default)
    UINavigationBar.appearance().shadowImage = UIImage()
    UINavigationBar.appearance().tintColor = .clear
}

And this in your child View

@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

Button(action: {self.presentationMode.wrappedValue.dismiss()}) {
    Image(systemName: "gobackward")
}

1 Comment

Thanks! This is an excellent solution for iOS 13 and iOS 14. Working in Xcode 12.5.1
6

Custom Back Button and Swipe Gesture Handling in SwiftUI in iOS 18 (Dec 27, 2024)

HomeScreen.swift

struct HomeScreen: View {
    var body: some View {
        NavigationStack {
            NavigationLink("Go To Detail", destination: DetailScreen())
            .font(.title)
            .navigationTitle("Home Screen")
        }
    }
}

This file defines the HomeScreen view with a NavigationStack and a NavigationLink to the DetailScreen.

DetailScreen.swift

struct DetailScreen: View {
    @Environment(\.dismiss) var dismiss
    
    var body: some View {
        VStack {}
        .navigationTitle("Detail View")
        .navigationBarBackButtonHidden(true)
        .toolbar {
            ToolbarItem(placement: .topBarLeading) {
                Button(action: {
                    dismiss()
                }) {
                    Label("Go Home", systemImage: "arrow.left")
                }
            }
        }
    }
}
  • This file defines the DetailScreen view.
  • The default back button is hidden using .navigationBarBackButtonHidden(true).
  • A custom back button is added to the toolbar with a leading placement. The button uses the dismiss environment value to go back to the previous screen.

NavigationGesture.swift

// Reference: https://github.com/zjinhu/Brick_SwiftUI/blob/main/Sources/Brick/Tools/NavigationGesture.swift

import SwiftUI
#if os(iOS)
import UIKit

extension View {
    public func disableSwipeBack() -> some View {
        self.background(
            DisableSwipeBackView()
        )
    }
    
    public func regainSwipeBack() -> some View {
        self.background(
            RegainSwipeBackView()
        )
    }
}

struct DisableSwipeBackView: UIViewControllerRepresentable {
    
    typealias UIViewControllerType = DisableSwipeBackViewController
    
    
    func makeUIViewController(context: Context) -> UIViewControllerType {
        UIViewControllerType()
    }
    
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
    }
}

class DisableSwipeBackViewController: UIViewController {
    
    override func didMove(toParent parent: UIViewController?) {
        super.didMove(toParent: parent)
        if let parent = parent?.parent,
           let navigationController = parent.navigationController,
           let interactivePopGestureRecognizer = navigationController.interactivePopGestureRecognizer {
            navigationController.view.removeGestureRecognizer(interactivePopGestureRecognizer)
        }
    }
}

struct RegainSwipeBackView: UIViewControllerRepresentable {
    
    typealias UIViewControllerType = RegainSwipeBackViewController
    
    
    func makeUIViewController(context: Context) -> UIViewControllerType {
        UIViewControllerType()
    }
    
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
        
    }
}

class RegainSwipeBackViewController: UIViewController {
    
    override func didMove(toParent parent: UIViewController?) {
        super.didMove(toParent: parent)
        if let parent = parent?.parent,
           let navigationController = parent.navigationController {
            navigationController.interactivePopGestureRecognizer?.isEnabled = true
        }
    }
}

extension UINavigationController: @retroactive UIGestureRecognizerDelegate {
    override open func viewDidLoad() {
        super.viewDidLoad()
        interactivePopGestureRecognizer?.delegate = self
    }

    public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        return viewControllers.count > 1
    }
}
#endif
  • This file contains extensions and custom view controllers to manage the swipe back gesture.
  • disableSwipeBack and regainSwipeBack functions are added to the View extension to enable or disable the swipe back gesture.
  • DisableSwipeBackView and RegainSwipeBackView are UIViewControllerRepresentable structs that manage the swipe back gesture.
  • DisableSwipeBackViewController and RegainSwipeBackViewController are custom view controllers that handle the enabling and disabling of the swipe back gesture.
  • The UINavigationController extension sets the gesture recognizer delegate to itself and ensures the swipe back gesture is only enabled when there is more than one view controller in the stack.

1 Comment

Only solution that works like a charm! Thanks!!!
4

Really simple method. Only two lines code 🔥

@Environment(\.presentationMode) var presentationMode
self.presentationMode.wrappedValue.dismiss()

Example:

import SwiftUI

struct FirstView: View {
    @State var showSecondView = false
    
    var body: some View {
        NavigationLink(destination: SecondView(),isActive : self.$showSecondView){
            Text("Push to Second View")
        }
    }
}


struct SecondView : View{
    @Environment(\.presentationMode) var presentationMode

    var body : some View {    
        Button(action:{ self.presentationMode.wrappedValue.dismiss() }){
            Text("Go Back")    
        }    
    }
}

Comments

3

All of the solutions I see here seem to disable swipe to go back functionality to navigate to the previous page, so sharing a solution I found that maintains that functionality. You can make an extension of your root view and override your navigation style and call the function in the view initializer.

Sample View

struct SampleRootView: View {

    init() {
        overrideNavigationAppearance()
    }

    var body: some View {
        Text("Hello, World!")
    }
}

Extension

extension SampleRootView {
   func overrideNavigationAppearance() {
        let navigationBarAppearance = UINavigationBarAppearance()
        let barAppearace = UINavigationBar.appearance()
        barAppearace.tintColor = *desired UIColor for icon*
        barAppearace.barTintColor = *desired UIColor for icon*

        navigationBarAppearance.setBackIndicatorImage(*desired UIImage for custom icon*, transitionMaskImage: *desired UIImage for custom icon*)

        UINavigationBar.appearance().standardAppearance = navigationBarAppearance
        UINavigationBar.appearance().compactAppearance = navigationBarAppearance
        UINavigationBar.appearance().scrollEdgeAppearance = navigationBarAppearance
   }
}

The only downfall to this approach is I haven't found a way to remove/change the text associated with the custom back button.

Comments

1

This solution works for iPhone. However, for iPad it won't work because of the splitView.

import SwiftUI

struct NavigationBackButton: View {
  var title: Text?
  @Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>

  var body: some View {
    ZStack {
      VStack {
        ZStack {
          HStack {
            Button(action: {
              self.presentationMode.wrappedValue.dismiss()
            }) {
              Image(systemName: "chevron.left")
                .font(.title)
                .frame(width: 44, height: 44)
              title
            }
            Spacer()
          }
        }
        Spacer()
      }
    }
    .zIndex(1)
    .navigationBarTitle("")
    .navigationBarHidden(true)
  }
}

struct NavigationBackButton_Previews: PreviewProvider {
  static var previews: some View {
    NavigationBackButton()
  }
}

Comments

1

On iOS 14+ it's actually very easy using presentationMode variable

In this example NewItemView will get dismissed on addItem completion:

struct NewItemView: View {
    @State private var itemDescription:String = ""
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    
    
    var body: some View {
        VStack {
            TextEditor(text: $itemDescription)
        }.onTapGesture {
            hideKeyboard()
        }.toolbar {
            
            ToolbarItem {
                Button(action: addItem){
                    Text("Save")
                }
            }
            
        }.navigationTitle("Add Question")
        
    }
    private func addItem() {
        // Add save logic
        // ...
        
        // Dismiss on complete
        presentationMode.wrappedValue.dismiss()
    }
    
    private func hideKeyboard() {
        UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
    }
}

struct NewItemView_Previews: PreviewProvider {
    static var previews: some View {
        NewItemView()
    }
}

In case you need the parent (Main) view:

struct SampleMainView: View {
    @Environment(\.managedObjectContext) private var viewContext
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \DbQuestion.timestamp, ascending: true)],
        animation: .default)
    private var items: FetchedResults<Item>
    
    var body: some View {
        NavigationView {
            List {
                ForEach(items) { item in
                    NavigationLink {
                        Text("This is item detail page")
                    } label: {
                        Text("Item at \(item.id)")
                    }
                }
                
            }
            .toolbar {
                ToolbarItem {
                        // Creates a button on toolbar
                        NavigationLink {
                            // New Item Page
                            NewItemView()
                        } label: {
                            Text("Add item")
                        }
                }
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                }
            }.navigationTitle("Main Screen")
            
        }
    }
}

Comments

0

I found this: https://ryanashcraft.me/swiftui-programmatic-navigation/

It does work, and it may lay the foundation for a state machine to control what is showing, but it is not a simple as it was before.

import Combine
import SwiftUI

struct DetailView: View {
    var onDismiss: () -> Void

    var body: some View {
        Button(
            "Here are details. Tap to go back.",
            action: self.onDismiss
        )
    }
}

struct RootView: View {
    var link: NavigationDestinationLink<DetailView>
    var publisher: AnyPublisher<Void, Never>

    init() {
        let publisher = PassthroughSubject<Void, Never>()
        self.link = NavigationDestinationLink(
            DetailView(onDismiss: { publisher.send() }),
            isDetail: false
        )
        self.publisher = publisher.eraseToAnyPublisher()
    }

    var body: some View {
        VStack {
            Button("I am root. Tap for more details.", action: {
                self.link.presented?.value = true
            })
        }
            .onReceive(publisher, perform: { _ in
                self.link.presented?.value = false
            })
    }
}

struct ContentView: View {
    var body: some View {
        NavigationView {
            RootView()
        }
    }
}

If you want to hide the button then you can replace the DetailView with this:

struct LocalDetailView: View {
    var onDismiss: () -> Void

    var body: some View {
        Button(
            "Here are details. Tap to go back.",
            action: self.onDismiss
        )
            .navigationBarItems(leading: Text(""))
    }
}

3 Comments

You know you can edit your answers? Don't post a new one but click the little edit button at the bottom of your answer. Then, delete this one.
Improved version. (Swift, iOS 13 beta 4) stackoverflow.com/questions/56853828/…
Interesting that I get blasted for posting a link to the point of getting my account severely limited, but this last answer gets through perfectly.
0

Just write this:

import SwiftUI

struct ContentView: View {
    var body: some View {
        NavigationView {

        }.onAppear() {
            UINavigationBar.appearance().tintColor = .clear
            UINavigationBar.appearance().backIndicatorImage = UIImage(named: "back")?.withRenderingMode(.alwaysOriginal)
            UINavigationBar.appearance().backIndicatorTransitionMaskImage = UIImage(named: "back")?.withRenderingMode(.alwaysOriginal)
        }
    }
}

1 Comment

it kinda "works" but the new back button image is not aligned correctly. The code might need some adjustments. Also, maybe it might be better to put it inside the init() {}?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.