iOS SwiftUI:以编程方式弹出或关闭视图
iOS SwiftUI: pop or dismiss view programmatically
我找不到任何有关以编程方式制作 pop 或 dismiss 我使用 SwiftUI 呈现的视图。
在我看来,唯一的方法是对模式使用已经集成的幻灯片操作(如果我想禁用此功能,则 what/how?),以及导航堆栈的后退按钮。
有人知道解决办法吗?
你知道这是一个错误还是会一直这样?
您可以尝试使用自定义视图和 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
。
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
)
}
}
如果您愿意,现在可以通过编程方式弹出 NavigationView。这是 beta 5。请注意,您不需要后退按钮。您可以以任何您喜欢的方式以编程方式触发 DetailView 中的 showSelf 属性。而且您不必在母版中显示 "Push" 文本。这可能是一个 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
此示例使用 Beta 5 发行说明中记录的新环境变量,该变量使用值 属性。它在后来的测试版中被更改为使用 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()
}
}
}
我在尝试对 presentationMode 绑定调用 value
时遇到编译器问题。将 属性 更改为 wrappedValue
为我解决了这个问题。我假设 value
-> wrappedValue
是语言更新。我认为此注释更适合作为对 Chuck H 的回答的评论,但没有足够的代表点来发表评论,我还建议将此更改为并进行编辑,但我的编辑被拒绝,因为它更适合作为评论或回答。
SwiftUI Xcode 测试版 5
首先,声明 @Environment,它有一个 dismiss 方法,您可以在任何地方使用它来关闭视图。
import SwiftUI
struct GameView: View {
@Environment(\.presentationMode) var presentation
var body: some View {
Button("Done") {
self.presentation.wrappedValue.dismiss()
}
}
}
或者,如果您不想通过按钮以编程方式执行此操作,则可以在需要弹出时从视图模型中发出。
订阅@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
}
}
我最近创建了一个名为 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()
}
}
结果是:
请检查以下代码,非常简单。
FirstView
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)
}
}
}
}
}
iOS 15
从iOS15开始我们可以使用新的@Environment(\.dismiss)
:
struct SheetView: View {
@Environment(\.dismiss) var dismiss
var body: some View {
NavigationView {
Text("Sheet")
.toolbar {
Button("Done") {
dismiss()
}
}
}
}
}
(不用再用presentationMode.wrappedValue.dismiss()
了。)
有用的链接:
这也会关闭视图
let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first as? UIWindowScene
let window = windowScene?.windows.first
window?.rootViewController?.dismiss(animated: true, completion: {
print("dismissed")
})
我找不到任何有关以编程方式制作 pop 或 dismiss 我使用 SwiftUI 呈现的视图。
在我看来,唯一的方法是对模式使用已经集成的幻灯片操作(如果我想禁用此功能,则 what/how?),以及导航堆栈的后退按钮。
有人知道解决办法吗? 你知道这是一个错误还是会一直这样?
您可以尝试使用自定义视图和 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
。
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
)
}
}
如果您愿意,现在可以通过编程方式弹出 NavigationView。这是 beta 5。请注意,您不需要后退按钮。您可以以任何您喜欢的方式以编程方式触发 DetailView 中的 showSelf 属性。而且您不必在母版中显示 "Push" 文本。这可能是一个 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
此示例使用 Beta 5 发行说明中记录的新环境变量,该变量使用值 属性。它在后来的测试版中被更改为使用 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()
}
}
}
我在尝试对 presentationMode 绑定调用 value
时遇到编译器问题。将 属性 更改为 wrappedValue
为我解决了这个问题。我假设 value
-> wrappedValue
是语言更新。我认为此注释更适合作为对 Chuck H 的回答的评论,但没有足够的代表点来发表评论,我还建议将此更改为并进行编辑,但我的编辑被拒绝,因为它更适合作为评论或回答。
SwiftUI Xcode 测试版 5
首先,声明 @Environment,它有一个 dismiss 方法,您可以在任何地方使用它来关闭视图。
import SwiftUI
struct GameView: View {
@Environment(\.presentationMode) var presentation
var body: some View {
Button("Done") {
self.presentation.wrappedValue.dismiss()
}
}
}
或者,如果您不想通过按钮以编程方式执行此操作,则可以在需要弹出时从视图模型中发出。 订阅@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
}
}
我最近创建了一个名为 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()
}
}
结果是:
请检查以下代码,非常简单。
FirstView
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)
}
}
}
}
}
iOS 15
从iOS15开始我们可以使用新的@Environment(\.dismiss)
:
struct SheetView: View {
@Environment(\.dismiss) var dismiss
var body: some View {
NavigationView {
Text("Sheet")
.toolbar {
Button("Done") {
dismiss()
}
}
}
}
}
(不用再用presentationMode.wrappedValue.dismiss()
了。)
有用的链接:
这也会关闭视图
let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first as? UIWindowScene
let window = windowScene?.windows.first
window?.rootViewController?.dismiss(animated: true, completion: {
print("dismissed")
})