SwiftUI中为NavigationView的导航栏自定义返回按钮

81

我想添加一个自定义导航按钮,大概看起来像这样:

期望的导航返回按钮

现在,我已经为此编写了一个自定义的BackButton视图。将该视图应用为前导导航条项时,执行以下操作:

.navigationBarItems(leading: BackButton())

导航视图看起来像这样:

当前导航返回按钮

我已经尝试过像这样的修改器:

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

没有任何运气。

问题

我如何...

  1. 设置用作导航栏自定义返回按钮的视图?或者:
  2. 以编程方式将视图弹回其父级?
    进行此方法时,我可以使用.navigationBarHidden(true)完全隐藏导航栏。

改进版。(Swift,iOS 13 beta 4)https://dev59.com/e7Tna4cB1Zd3GeqPBNoQ#57098885 - frogcjn
14个回答

154

简介

使用以下代码来转换到你的视图:

NavigationLink(destination: SampleDetails()) {}

将以下内容添加到视图本身中:
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

然后,在按钮操作或其他操作中,关闭该视图:

presentationMode.wrappedValue.dismiss()

完整代码

通过NavigationLink从父视图导航

 NavigationLink(destination: SampleDetails()) {}

在 DetailsView 中隐藏 navigationBarBackButton,并将自定义的返回按钮设置为前导 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)
    }
}

24
除了presentationMode.value现在已经变成presentationMode.wrappedValue之外,这个方法运行得很好。然而,这似乎禁用了默认的滑动返回行为。你有什么想法如何重新启用它? - banana1
12
如何添加滑动返回功能的任何想法? - Andrei Matei
谢谢,运行良好,但我们必须注意图像大小。始终记住使用.resizable()方法来自定义框架大小。 - iGhost
9
修复了滑动操作:https://dev59.com/XFIH5IYBdhLWcg3wiPVR#60067845 - daihovey
2
此解决方案已在iOS 15中被弃用。现在请使用DismissAction https://dev59.com/N1MI5IYBdhLWcg3wS5gv#72704145 - joshuakcockrell
显示剩余6条评论

28

SwiftUI - iOS 17

看起来你现在可以结合navigationBarBackButtonHidden.toolbar来实现你想要的效果。

代码

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")
                }
            }
        }
    }
}

示例

这是它的外观(摘自“SwiftUI Views Mastery”书中): SwiftUI中的自定义返回按钮


2
这将禁用滑动返回功能。 - undefined
这在我使用UIKitish导航控制器和包含SwiftUI视图作为视图控制器时不起作用。在其中一个子视图中,我需要完全隐藏导航栏,但仍然在SwiftUI中实现返回按钮,并且我仍然希望保持向右滑动返回的功能。 - undefined

23

iOS 15+

presentationMode.wrappedValue.dismiss()现已被弃用。

它已被DismissAction替代

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

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

您可以创建一个自定义的回退按钮,该按钮将使用此关闭操作。
struct NavBackButton: View {
    let dismiss: DismissAction
    
    var body: some View {
        Button {
            dismiss()
        } label: {
            Image("...custom back button here")
        }
    }
}

然后将其附加到您的视图。
.navigationBarBackButtonHidden(true) // Hide default button
.navigationBarItems(leading: NavBackButton(dismiss: self.dismiss)) // Attach custom button

你能解释一下这两个代码块之间的关联吗? - Javier Heisecke
@JavierHeisecke 顶部的代码块只是一个独立的示例,展示了如何取消。底部的两个代码块展示了如何添加一个自定义返回按钮,当点击时将会取消。 - joshuakcockrell
2
直截了当。正确的答案! - Kenan Begić
@joshuakcockrell 不再起作用了 :/ - Nikola.Lukovic
@Nikola.Lukovic 你是什么意思?我们正在生产环境中积极使用它,有几百个用户。检查一下你的iOS版本。你在NavigationView/NavigationStack中吗?制作一个最小的示例。将整个应用程序中除了主要的WindowGroup之外的所有内容都注释掉,并将其放入其中,以查看是否仍然存在问题。 - joshuakcockrell

11

根据其他答案,这是对于选项2的一个简化回答,适用于我在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)

    }
}
注意:要隐藏NavigationBar,我还需要在ContentView中设置并隐藏NavigationBar。
struct ContentView: View {
    var body: some View {
        NavigationView {
            VStack {
                NavigationLink(destination: DetailView()) {
                    Text("Link").padding()
                }
            } // Main VStack
            .navigationBarTitle("Home")
            .navigationBarHidden(true)

        } //NavigationView
    }
}

在XCode 11中非常顺畅!谢谢。 - Diego Navarro

9
你可以使用UIAppearance实现此功能:
if let image = UIImage(named: "back-button") {
    UINavigationBar.appearance().backIndicatorImage = image
    UINavigationBar.appearance().backIndicatorTransitionMaskImage = image
}

这应该尽早添加到你的应用程序中,例如App.init。 这还保留了本机返回刷卡功能。


8

通过其他评论中显示的原则,这里有一个更简洁的版本,仅更改按钮的文本。可以很容易地用另一个图标替换 chevron.left 图标。

创建自己的按钮,然后使用 .navigationBarItems() 进行分配。我发现以下格式最接近默认的返回按钮。

    @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")
            }
        }
    }

确保使用.navigationBarBackButtonHidden(true) 隐藏默认按钮并替换为自己的!

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

这个方法很好,但是有没有更好的方法可以在多个视图之间共享backButton? - BadmintonCat

6

这种方法无法禁用滑动。

对我有效。 XCode 11.3.1

将此代码放在您的根 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
}

并且这是在您的子视图中

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

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

1
谢谢!这是一个非常好的解决方案,适用于iOS 13和iOS 14。在Xcode 12.5.1中工作。 - Esteban

6

我想你希望在所有可导航的屏幕中使用自定义返回按钮,因此我根据@Ashish的答案编写了自定义包装器。

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
    }
}

将屏幕内容包装在NavigationItemContainer中:

使用方法:

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

你的CreateAccountScreenView出现了问题。如果在CreateAccountScreenView中有其他导航项,则只能使用尾部或前导导航项。这是因为你不能定义两次navigationBarItems,只有一个会起作用。 - LiangWang

4

非常简单的方法。只需要两行代码。

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

例子:

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")    
        }    
    }
}

3

我看到这里的所有解决方案似乎都禁用了滑动返回功能以导航到上一页,因此在此分享一个我找到的保持该功能的解决方案。您可以扩展根视图并覆盖导航样式,然后在视图初始化程序中调用该函数。

示例视图

struct SampleRootView: View {

    init() {
        overrideNavigationAppearance()
    }

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

扩展

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
   }
}

这种方法的唯一缺点是我尚未找到一种方法来删除/更改与自定义返回按钮相关联的文本。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接