SwiftUI - SwiftUI中是否有类似popViewController的功能?

104

我正在尝试使用SwiftUI,并希望在点击按钮时能够返回到先前的视图,就像我们在UINavigationController中使用popViewController一样。 到目前为止有提供这样做的方法吗?

我也尝试过使用NavigationDestinationLink来实现,但没有成功。

struct AView: View {
    var body: some View {
        NavigationView {
            NavigationButton(destination: BView()) {
                Text("Go to B")
            }
        }
    }
}

struct BView: View {
    var body: some View {
        Button(action: {
            // Trying to go back to the previous view
            // previously: navigationController.popViewController(animated: true)
        }) {
            Text("Come back to A")
        }
    }
}

可能是因为苹果想要阻止这种行为,让后退按钮成为唯一的返回方式。不过,这似乎是一个疏忽。 - mginn
@zoecarver 的想法是添加第二个视图(例如博客文章),并带有保存按钮以返回主视图并显示新项目,或者使用返回按钮取消操作。这只是我习惯的一种方式,但我可能可以将该视图嵌入弹出窗口或其他内容中。 - Alexandre Legent
7
我不明白为什么他们要阻止这样做。当您在列表中选择一个项目时,他们的本地Picker视图会展示出一个弹出行为。看起来API还没有最终确定下来,缺少某些东西。 - Toni Sučić
这可能会对您有所帮助,https://dev59.com/N1MI5IYBdhLWcg3wS5gv - Mohamed Eltantawy
对于iOS 16,有一个新的编程导航选项,即NavigationStack。请查看此答案:https://dev59.com/CMTsa4cB1Zd3GeqPINf7#76772102 - multitudes
显示剩余3条评论
12个回答

100

按照以下方式修改您的BView结构体。该按钮将像UIKit中的popViewController一样执行。

struct BView: View {
    @Environment(\.presentationMode) var presentationMode
    
    var body: some View {
        Button("Come back to A") {
            presentationMode.wrappedValue.dismiss()
        }
    }
}

2
value 现在已更改为 wrappedValue - LinusGeffarth
4
@Environment(\.presentationMode) var mode 是足够的,因为通过类型推断可以确定其类型。 - Alex Brown
如果我理解你的问题(提供一小段代码示例和真正的SO问题会更有帮助),你可以尝试让你的ObservableObject发出一个发布者,然后在你的视图中放置一个.OnReceive来进行dismiss操作。 - Chuck H
尽管Xcode 11在@Environment上出现错误,但我通过添加@SwiftUI.Environment使其正常工作。 - Vivienne Fosh
这对于从UIView推送到导航栈的包装SwiftUI视图无效。 - Aaron Bratcher
显示剩余6条评论

21

使用@Environment(\.presentationMode) var presentationMode返回上一个视图。 请查看下面的代码以更好地理解。

import SwiftUI

struct ContentView: View {


    var body: some View {

        NavigationView {
            ZStack {
                Color.gray.opacity(0.2)

                NavigationLink(destination: NextView(), label: {Text("Go to Next View").font(.largeTitle)})
            }.navigationBarTitle(Text("This is Navigation"), displayMode: .large)
                .edgesIgnoringSafeArea(.bottom)
        }
    }
}

struct NextView: View {
    @Environment(\.presentationMode) var presentationMode
    var body: some View {
        ZStack {
            Color.gray.opacity(0.2)
        }.navigationBarBackButtonHidden(true)
            .navigationBarItems(leading: Button(action: {
                self.presentationMode.wrappedValue.dismiss()
            }, label: { Image(systemName: "arrow.left") }))
            .navigationBarTitle("", displayMode: .inline)
    }
}


struct NameRow: View {
    var name: String
    var body: some View {
        HStack {
            Image(systemName: "circle.fill").foregroundColor(Color.green)
            Text(name)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

尝试使用按钮视图操作,效果也非常好。 - Ray Hunter
2
更新...使用:@Environment(.dismiss)var dismiss,以在操作中使用:dismiss()。 - Yan Cervantes

15

使用状态变量。尝试一下。

struct ContentViewRoot: View {
    @State var pushed: Bool = false
    var body: some View {
        NavigationView{
            VStack{
                NavigationLink(destination:ContentViewFirst(pushed: self.$pushed), isActive: self.$pushed) { EmptyView() }
                    .navigationBarTitle("Root")
                Button("push"){
                    self.pushed = true
                }
            }
        }
        .navigationViewStyle(StackNavigationViewStyle())
    }
}


struct ContentViewFirst: View {
    @Binding var pushed: Bool
    @State var secondPushed: Bool = false
    var body: some View {
        VStack{
            NavigationLink(destination: ContentViewSecond(pushed: self.$pushed, secondPushed: self.$secondPushed), isActive: self.$secondPushed) { EmptyView() }
                .navigationBarTitle("1st")
            Button("push"){
                self.secondPushed = true;
            }
        }
    }
}



struct ContentViewSecond: View {
    @Binding var pushed: Bool
    @Binding var secondPushed: Bool

    var body: some View {
        VStack{
            Spacer()
            Button("PopToRoot"){
                self.pushed = false
            } .navigationBarTitle("2st")

            Spacer()
            Button("Pop"){
                         self.secondPushed = false
                     } .navigationBarTitle("1st")
            Spacer()
        }
    }
}

在此输入图片描述


2
这是一个很好的答案,我只想补充一点,如果你把它放在表单或列表中,带有EmptyView的NavigationLink将占用一行。因此,为了避免空行,应该将NavigationLink和Button放在ZStack中。 - GJ Nilsen

10

这对我在watchOS上似乎有效(我还没有在iOS上尝试):

@Environment(\.presentationMode) var presentationMode

当你需要弹出时

self.presentationMode.wrappedValue.dismiss()

这样做会自动在1/2秒后弹出视图。 - Paresh. P
是的,它也适用于iOS。 - Bijender Singh Shekhawat

8

现在有一种编程方式可以弹出NavigationView,如果您愿意的话。这是beta 5中的新功能。

请注意,您不需要返回按钮。您可以以任何喜欢的方式编程触发DetailView中的showSelf属性。您也不必在主视图中显示“Push”文本。那可以是一个EmptyView(),从而创建一个不可见的segue。

(新的NavigationLink功能取代了已弃用的NavigationDestinationLink)

import SwiftUI

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

struct MasterView: View {
    @State var showDetail = false

    var body: some View {
        VStack {
            NavigationLink(destination: DetailView(showSelf: $showDetail), isActive: $showDetail) {
                Text("Push")
            }
        }
    }
}

struct DetailView: View {
    @Binding var showSelf: Bool

    var body: some View {
        Button(action: {
            self.showSelf = false
        }) {
            Text("Pop")
        }
    }
}

#if DEBUG
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
#endif

2
只有在使用Pop按钮时,这种方法才有效。如果点击Back按钮,则showSelf变量不会切换回false,MasterView将自动再次推送DetailView。请参见下面的答案,了解更清晰的解决方案。 - Chuck H
1
我发现这对我很有效。我可以使用返回按钮或弹出按钮并获得预期结果。 - dfrobison

4
似乎基本导航功能存在大量错误,这令人失望,可能值得暂时放弃,以避免花费数小时的沮丧。对我来说,“PresentationButton”是唯一可用的按钮。选项卡不正常工作,而“NavigationButton”在我的设备上根本无法使用。如果“NavigationButton”适用于您,则可能会有所不同。
希望他们能够修复自动完成问题的同时解决这个问题,这将为我们提供更好的了解。与此同时,我正在勉强绕过它并记下修复的注意事项。必须确认我们是否做错了什么或只是无法正常工作,但这就是测试版!

谢谢你的回答。我似乎没有你遇到的那么多麻烦。不过正如你所说,这仍然是测试版,还没有最终确定下来,但是 PresentationButton 目前对我很有用。 - Alexandre Legent

3
更新:此解决方案中的NavigationDestinationLink API已在iOS 13 Beta 5中被弃用。现在建议使用具有isActive绑定的NavigationLink。
我找到了一种使用NavigationDestinationLink在NavigationView中以编程方式推送/弹出视图的解决方案。
这是一个简单的示例:
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 MainView: 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 RootView: View {
    var body: some View {
        NavigationView {
            MainView()
        }
    }
}

我在这篇博客文章中写到了这个问题(链接)


1
beta 4 错误:类型 'AnyPublisher<Void, Never>' 的值没有成员 'send'。 - MobileMon
@Ryan Ashcraft。当我们有文本字段和按钮时,这种情况不起作用。如果文本字段已经填充,导航就会起作用,但是每当我们在文本字段上写字并按下按钮时,应用程序就会挂起,什么也不会发生,我们的动作没有起作用。我的场景是用于登录视图。 - Muhammad Danish Qureshi

3

你也可以使用.sheet来完成它

.navigationBarItems(trailing: Button(action: {
            self.presentingEditView.toggle()
        }) {
            Image(systemName: "square.and.pencil")
        }.sheet(isPresented: $presentingEditView) {
            EditItemView()
        })

在我的情况下,我将其用作右侧导航栏项,然后您需要创建要在该模态视图中显示的视图(在我的情况下是EditItemView())。

https://developer.apple.com/documentation/swiftui/view/sheet(ispresented:ondismiss:content:)


1

编辑:这里的答案比我的更好,但两者都可行:SwiftUI dismiss modal

你真正想要(或应该想要)的是模态呈现,这里有几个人提到了。如果你选择这条路,你肯定需要能够以编程方式解除模态,Erica Sadun在这里给出了一个很好的例子:https://ericasadun.com/2019/06/16/swiftui-modal-presentation/

考虑到声明式编码和命令式编码之间的差异,那里的解决方案可能不明显(例如将bool切换为false以关闭模态),但如果你的模型状态是真相来源,而不是UI本身的状态,那么它就有意义。

这是我对Erica的例子的快速看法,使用传递到TestModal中的绑定,以便它可以自行解除模态,而不必成为ContentView本身的成员(像Erica的简单性一样)。

struct TestModal: View {
    @State var isPresented: Binding<Bool>

    var body: some View {
        Button(action: { self.isPresented.value = false }, label: { Text("Done") })
    }
}

struct ContentView : View {
    @State var modalPresented = false

    var body: some View {
        NavigationView {
            Text("Hello World")
            .navigationBarTitle(Text("View"))
            .navigationBarItems(trailing:
                Button(action: { self.modalPresented = true }) { Text("Show Modal") })
        }
        .presentation(self.modalPresented ? Modal(TestModal(isPresented: $modalPresented)) {
            self.modalPresented.toggle()
        } : nil)
    }
}

1

以下代码在 XCode11 GM 中可行

self.myPresentationMode.wrappedValue.dismiss()

PresentationMode已经被弃用。 - pierre23

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