26

I've recently started working in SwiftUI, came to the conclusion that working with navigation isn't really great yet. What I'm trying to achieve is the following. I finally managed to get rid of the translucent background without making the application crash, but now I ran into the next issue. How can I get rid of the "back" text inside the navbaritem?

enter image description here

I achieved the view above by setting the default appearance in the SceneDelegate.swift file like this.

let newNavAppearance = UINavigationBarAppearance()
newNavAppearance.configureWithTransparentBackground()
newNavAppearance.setBackIndicatorImage(UIImage(named: "backButton"), transitionMaskImage: UIImage(named: "backButton"))
newNavAppearance.titleTextAttributes = [
    .font: UIFont(name: GTWalsheim.bold.name, size: 18)!,
    .backgroundColor: UIColor.white

]

UINavigationBar.appearance().standardAppearance = newNavAppearance

One possible way that I could achieve this is by overriding the navigation bar items, however this has one downside (SwiftUI Custom Back Button Text for NavigationView) as the creator of this issue already said, the back gesture stops working after you override the navigation bar items. With that I'm also wondering how I could set the foregroundColor of the back button. It now has the default blue color, however I'd like to overwrite this with another color.

Jordy
  • 948
  • 2
  • 9
  • 28

12 Answers12

48

Piggy-backing on the solution @Pitchbloas offered, this method just involves setting the backButtonDisplayMode property to .minimal:

extension UINavigationController {

  open override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    navigationBar.topItem?.backButtonDisplayMode = .minimal
  }

}
blau
  • 1,365
  • 15
  • 18
  • I didnt know about 'backButtonDisplayMode', thanks! –  Jun 04 '21 at 17:00
  • This is a great option! Just a note to others that it requires iOS 14+. – LordParsley Aug 05 '21 at 14:07
  • 2
    Found this to be a good solution, but should call `super.viewWillLayoutSubviews()` prior to setting the button display mode – D. Greg Dec 30 '21 at 14:03
  • 1
    This will change the behaviour of every back button in your app. If that is what you want, this is a great and simple solution, otherwise look at the other answers... – Daniel Jul 01 '22 at 18:16
39

It's actually really easy. The following solution is the fastest and cleanest i made.

Put this at the bottom of your SceneDelegate for example.

extension UINavigationController {
    // Remove back button text 
    open override func viewWillLayoutSubviews() {
        navigationBar.topItem?.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
    }
}

This will remove the back button text from every NavigationView (UINavigationController) in your app.

19

I have found a straightforward approach to remove the back button text using SwiftUI only, and keeping the original chevron.

A drag gesture is added to mimic the classic navigation back button when user wants to go back by swiping right. Following this, an extension of View is created to create a SwiftUI like modifier.

This is how to use it in code:

import SwiftUI

struct ContentView: View {

  var body: some View {
    ZStack {
      // Your main view code here with a ZStack to have the
      // gesture on all the view.
    }
    .navigationBarBackButtonTitleHidden()
  }
}

This is how to create the navigationBarBackButtonTitleHidden() modifier:

import SwiftUI

extension View {

  func navigationBarBackButtonTitleHidden() -> some View {
    self.modifier(NavigationBarBackButtonTitleHiddenModifier())
  }
}

struct NavigationBarBackButtonTitleHiddenModifier: ViewModifier {

  @Environment(\.dismiss) var dismiss

  @ViewBuilder @MainActor func body(content: Content) -> some View {
    content
      .navigationBarBackButtonHidden(true)
      .navigationBarItems(
        leading: Button(action: { dismiss() }) {
          Image(systemName: "chevron.left")
            .foregroundColor(.blue)
          .imageScale(.large) })
      .contentShape(Rectangle()) // Start of the gesture to dismiss the navigation
      .gesture(
        DragGesture(coordinateSpace: .local)
          .onEnded { value in
            if value.translation.width > .zero
                && value.translation.height > -30
                && value.translation.height < 30 {
              dismiss()
            }
          }
      )
  }
}
Roland Lariotte
  • 2,606
  • 1
  • 16
  • 40
  • This is awesome! I will mention that presentation.wrappedValue.dismiss has been a headache in a project, sometimes causing app crashes in specific instances. – Alex Hartford Jan 27 '22 at 18:44
  • Thanks. This can be refactored into a custom modifier: https://gist.github.com/janodev/9d765a827ba3af407e70cc380cffeb80 – Jano Dec 25 '22 at 18:32
  • @Jano I have updated the code following your suggestion – Roland Lariotte Dec 26 '22 at 09:27
4

If you want to:

  • Do it globally
  • Keep the standard back button (along with custom behaviours like an ability to navigate a few screens back on a long press)
  • Avoid introducing any third party frameworks

You can do it by setting the back button text color to Clear Color via appearance:

let navigationBarAppearance = UINavigationBarAppearance()

let backButtonAppearance = UIBarButtonItemAppearance(style: .plain)
backButtonAppearance.focused.titleTextAttributes = [.foregroundColor: UIColor.clear]
backButtonAppearance.disabled.titleTextAttributes = [.foregroundColor: UIColor.clear]
backButtonAppearance.highlighted.titleTextAttributes = [.foregroundColor: UIColor.clear]
backButtonAppearance.normal.titleTextAttributes = [.foregroundColor: UIColor.clear]

navigationBarAppearance.backButtonAppearance = backButtonAppearance

//Not sure you'll need both of these, but feel free to adjust to your needs.
UINavigationBar.appearance().standardAppearance = navigationBarAppearance
UINavigationBar.appearance().scrollEdgeAppearance = navigationBarAppearance

You can do it once when the app starts and forget about it.

A potential downside (depending on your preferences) is that the transition to the clear color is animated as the title of the current window slides to the left as you move to a different one.

You can also experiment with different text attributes.

Demo

FreeNickname
  • 7,398
  • 2
  • 30
  • 60
3

Standard Back button title is taken from navigation bar title of previous screen.

It is possible the following approach to get needed effect:

demo

struct TestBackButtonTitle: View {
    @State private var hasTitle = true
    var body: some View {
        NavigationView {
            NavigationLink("Go", destination:
                Text("Details")
                    .onAppear {
                        self.hasTitle = false
                    }
                    .onDisappear {
                        self.hasTitle = true
                    }
            )
            .navigationBarTitle(self.hasTitle ? "Master" : "")
        }
    }
}
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • 7
    Alright yes this is a possible solution, but it's really nasty, if I'd have to do that on EVERY screen it would be kind of annoying and hacky. I'm looking for a way to globally disable this text. – Jordy Mar 04 '20 at 14:51
3

So I actually ended up with the following solution that actually works. I am overwriting the navigation bar items like so

.navigationBarItems(leading:
    Image("backButton")
        .foregroundColor(.blue)
        .onTapGesture {
            self.presentationMode.wrappedValue.dismiss()
    }
)

The only issue with this was that the back gesture wasn't working so that was solved by actually extending the UINavigationController

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

    public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        return viewControllers.count > 1
    }
}

Now it's looking exactly the way I want it, the solution is kinda hacky... but it works for now, hopefully SwiftUI will mature a little bit so this can be done easier.

Jordy
  • 948
  • 2
  • 9
  • 28
  • Does't this give you duplicate navigation and back button on swipe back ? for me it was the case. So I ended up increasing navigation title width using toolbar. – Prafulla Apr 16 '21 at 05:42
2

Using the Introspect framework, you can easily gain access to the underlying navigation item and set the backButtonDisplayMode to minimal.

Here’s how you might use that in the view that was pushed

var body: some View {
    Text("Your body here")
        .introspectNavigationController { navController in
            navController.navigationBar.topItem?.backButtonDisplayMode = .minimal
        }
}
Aaron T
  • 159
  • 1
  • 6
2

Works on iOS 16

Solutions above didn't work for me. I wanted to make changes specific to view without any global (appearance or extension) and with minimal boilerplate code.

Since you can update NavigationItem inside the init of the View. You can solve this in 2 steps:

  1. Get visible View Controller.
// Get Visible ViewController
extension UIApplication {
    
    static var visibleVC: UIViewController? {
        var currentVC = UIApplication.shared.windows.first { $0.isKeyWindow }?.rootViewController
        while let presentedVC = currentVC?.presentedViewController {
            if let navVC = (presentedVC as? UINavigationController)?.viewControllers.last {
                currentVC = navVC
            } else if let tabVC = (presentedVC as? UITabBarController)?.selectedViewController {
                currentVC = tabVC
            } else {
                currentVC = presentedVC
            }
        }
        return currentVC
    }
    
}
  1. Update NavigationItem inside init of the View.
struct YourView: View {
    
    init(hideBackLabel: Bool = true) {
        if hideBackLabel {
            // iOS 14+
            UIApplication.visibleVC?.navigationItem.backButtonDisplayMode = .minimal
            
            // iOS 13-
            let button = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
            UIApplication.visibleVC?.navigationItem.backBarButtonItem = button
        }
    }

}
k-thorat
  • 4,873
  • 1
  • 27
  • 36
0

custom navigationBarItems and self.presentationMode.wrappedValue.dismiss() worked but you are not allow to perform swiping back

You can either add the following code to make the swipe back again

//perform gesture go back
extension UINavigationController: UIGestureRecognizerDelegate {
    override open func viewDidLoad() {
        super.viewDidLoad()
        interactivePopGestureRecognizer?.delegate = self
    }

    public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        return viewControllers.count > 1
    }
}

but the problem is, sometimes it will make your app crashed when you swipe half the screen and then cancel.

I would suggest the other way to remove the "Back" text. Adding the isActive state to monitor whether the current screen is active or not. :)

struct ContentView: View {
    @State var isActive = false

    var body: some View {
        NavigationView() {
            NavigationLink(
                "Next",
                destination: Text("Second Page").navigationBarTitle("Second"),
                isActive: $isActive
            )
                .navigationBarTitle(!isActive ? "Title" : "", displayMode: .inline)
        }
    }
}
JUsltop
  • 44
  • 2
  • On iOS 15.2 this (the second fix using the $isActive binding) works to disable the back button text but it causes a random item to be selected on navigation and the debug console shows the error "SwiftUI encountered an issue when pushing aNavigationLink. Please file a bug." – tdl Dec 31 '21 at 22:40
0

I am accomplishing this by changing the title of the master screen before pushing the detail screen and then setting it back when it re-appears. The only caveat is when you go back to the master screen the title's re-appearance is a little noticeable.

Summary:

  • on master view add state var (e.g. isDetailShowing) to store if detail screen is showing or not

  • on master view use the navigationTitle modifier to set the title based on the current value of isDetailShowing

  • on master view use onAppear modifier to set the value of isDetailShowing to false

  • on the NavigationLink in master screen use the simultaneousGesture modifier to set the isDetailShowing to true

    struct MasterView: View {
      @State var isDetailShowing = false
    
      var body: some View {
    
          VStack {
              Spacer()
                  .frame(height: 20)
              Text("Master Screen")
                  .frame(maxWidth: .infinity, alignment: .leading)
              Spacer()
                  .frame(height: 20)
    
              NavigationLink(destination: DetailView()) {
                  Text("Go to detail screen")
              }
              .simultaneousGesture(TapGesture().onEnded() {
                  isDetailShowing = true
              })
          }
          .navigationBarTitleDisplayMode(.inline)
          .navigationTitle(isDetailShowing ? "" : "Master Screen Title")
          .onAppear() {
              isDetailShowing = false
          }
      }
    }
    
    struct DetailView: View {
      var body: some View {
          Text("This is the detail screen")
          .navigationBarTitleDisplayMode(.inline)
          .navigationTitle("Detail Screen Title")
      }
    }
    
cigien
  • 57,834
  • 11
  • 73
  • 112
DCDC
  • 486
  • 5
  • 9
0

you can use .toolbarRole(.editor)

yilmazdincsoy
  • 61
  • 1
  • 3
-2

Why not use Custom BackButton with Default Back Button Hidden

You could use Any Design You Prefer !

Works on iOS 16

First View

struct ContentView: View {
var body: some View {
    NavigationView {
        VStack(){
            Spacer()
            NavigationLink(destination: View2()) {
                Text("Navigate")
                    .font(.title)
            }
            Spacer()
        }
    }
}
}

Second View

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

var body: some View {
    NavigationView {
        ZStack{
            VStack{
                HStack(alignment:.center){
//Any Design You Like
                    Image(systemName: "chevron.left")
                           .font(.title)
                           .foregroundColor(.blue)
                           .onTapGesture {                          
self.presentationMode.wrappedValue.dismiss()
                           }
                           .padding()
                   Spacer()
                }
                Spacer()
            }
        }
    }
    .navigationBarBackButtonHidden(true)
}
}