【问题标题】:iOS SwiftUI: pop or dismiss view programmaticallyiOS SwiftUI:以编程方式弹出或关闭视图
【发布时间】:2019-10-24 02:20:05
【问题描述】:

我找不到任何有关使用 SwiftUI 以popdismiss以编程方式呈现我的视图的任何方法的参考。

在我看来,唯一的方法是对模式使用已经集成的滑动向下操作(如果我想禁用此功能该怎么办/如何?),以及导航堆栈的后退按钮。

有人知道解决办法吗? 你知道这是一个错误还是会一直这样?

【问题讨论】:

  • 鉴于当前的 API 状态,您必须自己实现这些转换。
  • 您现在可以在 Beta 5 中为 Navigation 和 Modals 执行此操作。请参阅下面的答案。
  • 看看这个开源项目:github.com/biobeats/swiftui-navigation-stack 它是 SwiftUI 的替代导航堆栈,除其他外,它提供了以编程方式推送/弹出的可能性。如果你们能和我一起改进这个项目,那就太好了。
  • @Andrea,你解决了吗?我还是卡在这里
  • 在这里您可以通过示例找到最简单的答案????:
    stackoverflow.com/a/62863487/12534983

标签: ios swift swiftui


【解决方案1】:

此示例使用 Beta 5 发行说明中记录的新环境变量,该变量使用 value 属性。它在后来的测试版中被更改为使用 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()
        }
    }
}

【讨论】:

  • 这真是太好了!我只是希望它也适用于双列导航,让我们在用户以纵向模式启动 iPad 时看到拆分视图的侧边栏。
  • 我认为这应该是公认的答案。它非常干净,不需要对父视图进行任何修改。
  • 这是一个很棒的 iOS 解决方案,我知道这是 OP 的主要目标。遗憾的是,它似乎不适用于 macOS 导航列表,其中列表和视图同时显示。有什么已知的方法吗?
  • 如何从按钮调用RootView?
  • 我想这就是你想要的:stackoverflow.com/questions/57334455/…
【解决方案2】:

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

【讨论】:

  • 太棒了;最简单的解决方案。应该在顶部。
  • 适用于 iOS 13、Swift 5。不错的简单解决方案!
【解决方案3】:

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()。)


有用的链接:

【讨论】:

  • 作为 Swift 的新手,我在逻辑上完全不理解这种模式。但它有效,而且我的应用程序将仅适用于 iOS 15,非常感谢!
【解决方案4】:

如果您愿意,现在可以通过编程方式弹出 NavigationView。这是 beta 5。请注意,您不需要后退按钮。您可以以任何您喜欢的方式以编程方式触发 DetailView 中的 showSelf 属性。而且您不必在母版中显示“推送”文本。这可能是一个 EmptyView(),从而创建了一个不可见的 segue。

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 的导航视图中按后退按钮而不是按“弹出”按钮,这会导致我出错。任何想法如何解决?
  • 如果您使用此方法,您需要隐藏后退按钮,这样它就不会干扰您以编程方式弹出视图。不是真正的修复,但绝对是避免问题的一种方法。
  • 我希望 beta 6 解决了这个问题
  • 要添加这个很好的答案,如果您使用 tag: , selection: 初始化而不是 isActive: 初始化,您还可以将此选择作为绑定变量传递并将其设置为 nil(或某个值除了你的标签)来弹出视图。
  • 我的评论的附录。这对我来说是一个巨大的教训:为了能够弹出 2 个或更多,您需要将 .isDetailLink(false) 添加到根导航链接。否则,当堆栈中的第三个视图出现时,选择会自动设置为 nil。
【解决方案5】:

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

首先将您的层次结构嵌入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()
    }
}

结果是:

【讨论】:

  • 刚刚试了一下,非常棒,它比内置的 SwiftUI 堆栈可靠得多。我递归地将最多 20 个屏幕副本推送到堆栈上,而内置的副本变得混乱,您的 NavigationStack 可以完美地处理它。
  • 我们如何使用 self.navStack.pop() 只弹出 2 个屏幕
  • @AliRehman 如果你想跳到一个特定的视图(而不仅仅是前一个),你必须给那个视图一个标识符。例如:PushView(destination: Child0(), destinationId: "destinationId") { Text("PUSH") },然后是 PopView(destination: .view(withId: "destinationId")) { Text("POP") }。如果您以编程方式访问导航堆栈环境对象,则相同。
  • @matteopuc 谢谢我打了两次电话来弹出 2 个屏幕。像 self.navStack.pop(); self.navStack.pop();它正在工作,现在根据您的建议更新它!
【解决方案6】:

或者,如果您不想通过按钮以编程方式执行此操作,则可以在需要弹出时从视图模型中发出。 订阅一个@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
    }
}

【讨论】:

    【解决方案7】:

    请检查以下代码,它是如此简单。

    第一视图

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

    第二视图

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

    【讨论】:

    • 这是现在最好的答案,但请注意,您可以只传入一个监视选择的环境对象,然后将任何后续子视图中的选择设置为 nil 以返回根视图,而不是使用演示模式.
    【解决方案8】:

    您可以尝试使用自定义视图和Transition

    这是一个自定义模式。

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

    我重用了Animation Views and Transition 教程中的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() }
            }
        }
    
    }
    

    多亏了这种转换,您将看到模态 sliding in from the trailing edge,它会看到 zoom and fade out when it is dismissed

    【讨论】:

    • 感谢 Matteo,我会尽快尝试,这可能是一个很酷的临时解决方法,希望苹果会引入解雇和弹出
    【解决方案9】:

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

    【讨论】:

    • 如果用户关闭模态向下滑动怎么办?状态停留在错误的状态。并且没有办法为向下滑动手势添加侦听器。我很确定将在下一个版本中扩展此 pop/dismiss 功能
    • 试试onDisappear(_:) ?
    【解决方案10】:

    我在尝试对presentationMode 绑定调用value 时遇到了编译器问题。将属性更改为 wrappedValue 为我解决了这个问题。我假设 value -> wrappedValue 是语言更新。我认为这个注释更适合作为对 Chuck H 答案的评论,但没有足够的代表点来评论,我还建议将此更改为和编辑,但我的编辑被拒绝,因为它更适合作为评论或答案。

    【讨论】:

      猜你喜欢
      • 2020-05-24
      • 2016-07-22
      • 1970-01-01
      • 2020-11-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多