iOS SwiftUI:如何以编程方式弹出或解除视图?

158

我找不到任何关于如何以编程方式制作 SwiftUI 中呈现视图的 弹出消失 的参考资料。

在我看来,唯一的方法是使用模态的已集成下滑操作(如果我想禁用此功能,该怎么做?),以及导航栈的返回按钮。

有人知道解决方案吗? 您是否知道这是一个错误还是会保持这样?


看看这个开源项目:https://github.com/biobeats/swiftui-navigation-stack 它是SwiftUI的另一种导航栈,除其他功能外,它还提供了编程推送/弹出的可能性。如果你们加入我一起改进这个项目,那就太好了。 - superpuccio
在这里,您可以找到简单的答案和示例: <br> https://dev59.com/N1MI5IYBdhLWcg3wS5gv#62863487 - Sapar Friday
现在最好的答案是使用navigationLink标签和selection,并使用跟踪选择的environmentobject。然后,您可以将environmentobjectselection设置为nil,以从任何子视图返回到根视图。 - Super Noob
从iOS 15开始,我们可以使用DismissAction - 参见这个答案 - pawello2222
请查看 SwiftUI 导航库 github.com/canopas/UIPilot,以便轻松导航。它不会替换 NavigationView,但是以一种非常易于使用的方式包装了 NavigationView - jimmy0251
显示剩余3条评论
11个回答

188

这个示例使用了Beta 5发布说明中记录的新环境变量,该变量原本使用value属性。后来在后续的beta版本中更改为使用wrappedValue属性。现在,此示例已更新至GM版本。同样的概念也适用于使用.sheet修饰符呈现的模态视图的取消。

import SwiftUI

struct DetailView: View {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    var body: some View {
        Button(
            "Here is Detail View. Tap to go back.",
            action: { self.presentationMode.wrappedValue.dismiss() }
        )
    }
}

struct RootView: View {
    var body: some View {
        VStack {
            NavigationLink(destination: DetailView())
            { Text("I am Root. Tap for Detail View.") }
        }
    }
}

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

2
这真的很好!我只希望它也适用于双列导航,以便让我们在用户将iPad竖屏启动时看到分割视图的侧边栏。 - MScottWaller
1
我认为这应该是被接受的答案。它非常干净,不需要对父视图进行任何修改。 - Maetthu24
2
这是一个很棒的iOS解决方案,我知道这是OP的主要目标。但遗憾的是,在macOS导航列表中似乎不起作用,因为列表和视图同时显示。有没有已知的方法可以解决这个问题? - TheNeil
我认为这就是你想要的:https://dev59.com/21MH5IYBdhLWcg3w_Wd_#59662275 - Chuck H
这个非常有效,但我没有看到任何过渡效果。视图只是立即消失了。 - alionthego
显示剩余5条评论

121

SwiftUI Xcode Beta 5

首先,声明@Environment,其中包含一个dismiss方法,您可以在任何地方使用它来关闭视图。

import SwiftUI

struct GameView: View {
    
    @Environment(\.presentationMode) var presentation
    
    var body: some View {
        Button("Done") {
            self.presentation.wrappedValue.dismiss()
        }
    }
}

5
非常棒,最简单的解决方案。应该置顶显示。 - ty1
3
可用于iOS 13和Swift 5。解决方案简单易懂! - user3069232
1
我想自动关闭视图,而不需要点击按钮。我们能做到吗? - Niharika
是的,在您想要应用程序关闭视图时,只需编写self.presentation.wrappedValue.dismiss()即可。 - Prashant Gaikwad

116

iOS 15+

从iOS 15开始,我们可以使用一个新的@Environment(\.dismiss)

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

    var body: some View {
        NavigationView {
            Text("Sheet")
                .toolbar {
                    Button("Done") {
                        dismiss()
                    }
                }
        }
    }
}

(不再需要使用 presentationMode.wrappedValue.dismiss()。)


有用的链接:


4
作为一个相对新手的Swift开发者,我对这个模式完全没什么逻辑理解。但它确实有效,而且我的应用程序将仅支持iOS 15,所以非常感谢你! - Jason Farnsworth
1
如果您想在watchOS中关闭NavigationLink,这也非常有效。感谢您指引我到这里! - Rob McQualter

23

现在有一种编程方式可以弹出NavigationView,如果您愿意的话。这是beta 5版中的新功能。请注意,您不需要返回按钮。您可以以任何想要的方式编程触发DetailView中的showSelf属性。而且您不必在主视图中显示“Push”文本,那可以是一个EmptyView(),从而创建一个无形的转场。

import SwiftUI

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

struct MasterView: View {
    @State private 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

如果我从detailView的导航视图中按下返回按钮而不是按下“pop”按钮,这会给我带来错误。有什么想法如何修复? - MobileMon
在使用此方法的情况下,您将希望隐藏返回按钮,以便它不会干扰您编程方式弹出视图的方式。这并不是真正的解决方法,但绝对是避免问题的一种方式。 - MScottWaller
我希望beta 6可以解决这个问题。 - MobileMon
补充一下,如果你使用的是 tag: , selection: 初始化方式而不是 isActive:,你也可以将这个选择作为绑定变量传递,并将其设置为 nil(或其他与你的标签不同的值)以弹出视图。 - AlexMath
2
我的评论补充。这是我学到的重要课程:为了能够弹出2个或更多,您需要将.isDetailLink(false)添加到根导航链接。否则,当堆栈中出现第3个视图时,选择会自动设置为nil。 - AlexMath

10

我最近创建了一个开源项目,叫做 swiftui-navigation-stackhttps://github.com/biobeats/swiftui-navigation-stack),其中包含 NavigationStackView,这是 SwiftUI 的一种替代导航栈。它提供了几个在代码库的自述文件中描述的功能。例如,你可以轻松地以编程方式推送和弹出视图。我将通过一个简单的例子向你展示如何实现:

首先,在 NavigationStackVew 中嵌入你的视图层次结构:

struct RootView: View {
    var body: some View {
        NavigationStackView {
            View1()
        }
    }
}

NavigationStackView为您的层次结构提供了一个有用的环境对象,称为NavigationStack。您可以使用它来编程方式弹出视图,就像上面问题中所要求的一样:

struct View1: View {
    var body: some View {
        ZStack {
            Color.yellow.edgesIgnoringSafeArea(.all)
            VStack {
                Text("VIEW 1")
                Spacer()

                PushView(destination: View2()) {
                    Text("PUSH TO VIEW 2")
                }
            }
        }
    }
}

struct View2: View {
    @EnvironmentObject var navStack: NavigationStack
    var body: some View {
        ZStack {
            Color.green.edgesIgnoringSafeArea(.all)
            VStack {
                Text("VIEW 2")
                Spacer()

                Button(action: {
                    self.navStack.pop()
                }, label: {
                    Text("PROGRAMMATICALLY POP TO VIEW 1")
                })
            }
        }
    }
}
在这个例子中,我使用PushView来触发轻触后推送导航。然后,在View2中,我使用环境对象来编程式地返回。
这是完整的例子:
import SwiftUI
import NavigationStack

struct RootView: View {
    var body: some View {
        NavigationStackView {
            View1()
        }
    }
}

struct View1: View {
    var body: some View {
        ZStack {
            Color.yellow.edgesIgnoringSafeArea(.all)
            VStack {
                Text("VIEW 1")
                Spacer()

                PushView(destination: View2()) {
                    Text("PUSH TO VIEW 2")
                }
            }
        }
    }
}

struct View2: View {
    @EnvironmentObject var navStack: NavigationStack
    var body: some View {
        ZStack {
            Color.green.edgesIgnoringSafeArea(.all)
            VStack {
                Text("VIEW 2")
                Spacer()

                Button(action: {
                    self.navStack.pop()
                }, label: {
                    Text("PROGRAMMATICALLY POP TO VIEW 1")
                })
            }
        }
    }
}

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

结果是:

在此输入图像描述


如何使用self.navStack.pop()弹出仅两个屏幕? - AliRehman7141
1
@AliRehman 如果你想要跳转到特定的视图(而不仅仅是上一个视图),你必须给那个视图一个标识符。例如:PushView(destination: Child0(), destinationId: "destinationId") { Text("PUSH") } 然后 PopView(destination: .view(withId: "destinationId")) { Text("POP") }。如果你以编程方式访问导航堆栈环境对象,也是同样的方法。 - superpuccio
@matteopuc 谢谢,我调用了两次pop方法来弹出两个屏幕,就像这样: self.navStack.pop(); self.navStack.pop(); 现在根据你的建议已经更新了! - AliRehman7141
1
如果你想使用路由器(强烈推荐)或编程方式的推送和弹出,每个人都应该使用这个。感谢这个易于理解、优雅的 SwiftUI 导航解决方案,其中没有任何魔法 :) - Andrew

7

或者,如果您不想从按钮程序方式实现它,您可以在需要弹出时从视图模型中发出信号。 订阅一个@Published,每当保存完成时更改其值。

struct ContentView: View {
    @ObservedObject var viewModel: ContentViewModel
    @Environment(\.presentationMode) var presentationMode

    init(viewModel: ContentViewModel) {
        self.viewModel = viewModel
    }

    var body: some View {
        Form {
            TextField("Name", text: $viewModel.name)
                .textContentType(.name)
        }
        .onAppear {
            self.viewModel.cancellable = self.viewModel
                .$saved
                .sink(receiveValue: { saved in
                    guard saved else { return }
                    self.presentationMode.wrappedValue.dismiss()
                }
            )
        }
    }
}

class ContentViewModel: ObservableObject {
    @Published var saved = false // This can store any value.
    @Published var name = ""
    var cancellable: AnyCancellable? // You can use a cancellable set if you have multiple observers.

    func onSave() {
        // Do the save.

        // Emit the new value.
        saved = true
    }
}

6

请查看以下代码,它非常简单。

首个视图

struct StartUpVC: View {
@State var selection: Int? = nil

var body: some View {
    NavigationView{
        NavigationLink(destination: LoginView().hiddenNavigationBarStyle(), tag: 1, selection: $selection) {
            Button(action: {
                print("Signup tapped")
                self.selection = 1
            }) {
                HStack {
                    Spacer()
                    Text("Sign up")
                    Spacer()
                }
            }
        }
    }
}

SecondView

struct LoginView: View {
@Environment(\.presentationMode) var presentationMode
    
var body: some View {
    NavigationView{
        Button(action: {
           print("Login tapped")
           self.presentationMode.wrappedValue.dismiss()
        }) {
           HStack {
              Image("Back")
              .resizable()
              .frame(width: 20, height: 20)
              .padding(.leading, 20)
           }
        }
      }
   }
}

这是目前最好的答案,但请注意,您可以使用监视selection的环境对象而不是使用presentationMode,并从任何后续子视图中将selection设置为nil以返回根视图。 - Super Noob

3
您可以尝试使用自定义视图和过渡效果来实现。
以下是一个自定义模态窗口。
struct ModalView<Content>: View where Content: View {

    @Binding var isShowing: Bool
    var content: () -> Content

    var body: some View {
        GeometryReader { geometry in
            ZStack(alignment: .center) {
                if (!self.isShowing) {
                    self.content()
                }
                if (self.isShowing) {
                    self.content()
                        .disabled(true)
                        .blur(radius: 3)

                    VStack {
                        Text("Modal")
                    }
                    .frame(width: geometry.size.width / 2,
                           height: geometry.size.height / 5)
                    .background(Color.secondary.colorInvert())
                    .foregroundColor(Color.primary)
                    .cornerRadius(20)
                    .transition(.moveAndFade) // associated transition to the modal view
                }
            }
        }
    }

}

我重用了来自动画视图和转换教程中的Transition.moveAndFade

它定义如下:

extension AnyTransition {
    static var moveAndFade: AnyTransition {
        let insertion = AnyTransition.move(edge: .trailing)
            .combined(with: .opacity)
        let removal = AnyTransition.scale()
            .combined(with: .opacity)
        return .asymmetric(insertion: insertion, removal: removal)
    }
}

您可以这样测试它——在模拟器中,而不是预览中:
struct ContentView: View {

    @State var isShowingModal: Bool = false

    func toggleModal() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
            withAnimation {
                self.isShowingModal = true
            }
            DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
                withAnimation {
                    self.isShowingModal = false
                }
            }
        }
    }

    var body: some View {
        ModalView(isShowing: $isShowingModal) {
            NavigationView {
                List(["1", "2", "3", "4", "5"].identified(by: \.self)) { row in
                    Text(row)
                }.navigationBarTitle(Text("A List"), displayMode: .large)
            }.onAppear { self.toggleModal() }
        }
    }

}

感谢这个变化,您将看到该弹出窗口从尾部滑入,当关闭弹出窗口时,它将缩小并淡出。

1
谢谢Matteo,我会尽快尝试这个方法,希望苹果公司能够推出dismiss和pop功能,这可能是一个很酷的临时解决方法。 - Andrea Miotto

1

这也将关闭视图

       let scenes = UIApplication.shared.connectedScenes
                let windowScene = scenes.first as? UIWindowScene
                let window = windowScene?.windows.first
                
                window?.rootViewController?.dismiss(animated: true, completion: {
                    print("dismissed")
                })

1
SwiftUI的核心概念是关注数据流动。
您需要使用@State变量并改变该变量的值来控制弹出和关闭。
struct MyView: View {
    @State
    var showsUp = false

    var body: some View {
        Button(action: { self.showsUp.toggle() }) {
            Text("Pop")
        }
        .presentation(
            showsUp ? Modal(
                Button(action: { self.showsUp.toggle() }) {
                    Text("Dismiss")
                }
            ) : nil
        )
    }
}


如果用户通过向下滑动关闭模态框,状态将保持在错误状态。而且没有一种方法可以添加监听器来检测向下滑动手势。我相信他们会在下一个版本中扩展这个弹出/关闭功能。 - Andrea Miotto
尝试使用 onDisappear(_:) - WeZZard

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