最后,现在有了Beta 5,我们可以以编程方式弹出到父视图。但是,在我的应用程序中,有几个地方视图都有一个“保存”按钮,该按钮可以结束几个步骤并返回到开始。在UIKit中,我使用popToRootViewController(),但是我一直无法找到在SwiftUI中执行相同操作的方法。
以下是我尝试实现的模式的简单示例。有任何想法吗?
import SwiftUI
struct DetailViewB: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
VStack {
Text("This is Detail View B.")
Button(action: { self.presentationMode.value.dismiss() } )
{ Text("Pop to Detail View A.") }
Button(action: { /* How to do equivalent to popToRootViewController() here?? */ } )
{ Text("Pop two levels to Master View.") }
}
}
}
struct DetailViewA: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
VStack {
Text("This is Detail View A.")
NavigationLink(destination: DetailViewB() )
{ Text("Push to Detail View B.") }
Button(action: { self.presentationMode.value.dismiss() } )
{ Text("Pop one level to Master.") }
}
}
}
struct MasterView: View {
var body: some View {
VStack {
Text("This is Master View.")
NavigationLink(destination: DetailViewA() )
{ Text("Push to Detail View A.") }
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
MasterView()
}
}
}
Run Code Online (Sandbox Code Playgroud)
mal*_*hal 155
将视图修饰符设置isDetailLink
为false
on aNavigationLink
是让 pop-to-root 工作的关键。isDetailLink
是true
默认的并且适应包含视图。例如,在 iPad 横向上,拆分视图是分开的,并isDetailLink
确保目标视图将显示在右侧。因此设置isDetailLink
为false
意味着目标视图将始终被推送到导航堆栈上;因此总是可以弹出。
随着设置isDetailLink
为false
on NavigationLink
,将isActive
绑定传递到每个后续目标视图。最后当你想弹出到根视图时,将值设置为false
它会自动弹出所有内容:
import SwiftUI
struct ContentView: View {
@State var isActive : Bool = false
var body: some View {
NavigationView {
NavigationLink(
destination: ContentView2(rootIsActive: self.$isActive),
isActive: self.$isActive
) {
Text("Hello, World!")
}
.isDetailLink(false)
.navigationBarTitle("Root")
}
}
}
struct ContentView2: View {
@Binding var rootIsActive : Bool
var body: some View {
NavigationLink(destination: ContentView3(shouldPopToRootView: self.$rootIsActive)) {
Text("Hello, World #2!")
}
.isDetailLink(false)
.navigationBarTitle("Two")
}
}
struct ContentView3: View {
@Binding var shouldPopToRootView : Bool
var body: some View {
VStack {
Text("Hello, World #3!")
Button (action: { self.shouldPopToRootView = false } ){
Text("Pop to root")
}
}.navigationBarTitle("Three")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Run Code Online (Sandbox Code Playgroud)
Chu*_*k H 30
当然,@malhal 是解决方案的关键,但对我来说,将 Binding 作为参数传递到 View 中是不切实际的。正如@Imthath 所指出的,环境是一种更好的方式。
这是模仿 Apple 发布的dismiss() 方法以弹出到上一个视图的另一种方法。
定义环境的扩展:
struct RootPresentationModeKey: EnvironmentKey {
static let defaultValue: Binding<RootPresentationMode> = .constant(RootPresentationMode())
}
extension EnvironmentValues {
var rootPresentationMode: Binding<RootPresentationMode> {
get { return self[RootPresentationModeKey.self] }
set { self[RootPresentationModeKey.self] = newValue }
}
}
typealias RootPresentationMode = Bool
extension RootPresentationMode {
public mutating func dismiss() {
self.toggle()
}
}
Run Code Online (Sandbox Code Playgroud)
用法:
添加.environment(\.rootPresentationMode, self.$isPresented)
到根NavigationView
,其中isPresented
被Bool
用于呈现所述第一子图。
将.navigationViewStyle(StackNavigationViewStyle())
修饰符添加到 root NavigationView
,或添加.isDetailLink(false)
到NavigationLink
第一个子视图。
@Environment(\.rootPresentationMode) private var rootPresentationMode
从应该执行 pop 到 root 的位置添加到任何子视图。
最后,self.rootPresentationMode.wrappedValue.dismiss()
从该子视图调用将弹出到根视图。
我在 GitHub 上发布了一个完整的工作示例:
https://github.com/Whiffer/SwiftUI-PopToRootExample
mic*_*cah 14
iOS 16 解决方案
现在终于可以用新添加的内容弹出到根视图了NavigationStack
!
struct DataObject: Identifiable, Hashable {
let id = UUID()
let name: String
}
@available(iOS 16.0, *)
struct ContentView8: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
Text("Root Pop")
.font(.largeTitle)
.foregroundColor(.primary)
NavigationLink("Click Item", value: DataObject.init(name: "Item"))
.listStyle(.plain)
.navigationDestination(for: DataObject.self) { course in
Text(course.name)
NavigationLink("Go Deeper", value: DataObject.init(name: "Item"))
Button("Back to root") {
path = NavigationPath()
}
}
}
.padding()
}
}
Run Code Online (Sandbox Code Playgroud)
Sup*_*oob 12
女士们,先生们,介绍 Apple 对这个问题的解决方案。 *也通过 HackingWithSwift 呈现给你(我从 lol 那里偷了这个):在程序化导航下
(在 Xcode 12 和 iOS 14 上测试)
基本上你使用tag
and selection
insidenavigationlink
直接进入你想要的任何页面。
struct ContentView: View {
@State private var selection: String? = nil
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: Text("Second View"), tag: "Second", selection: $selection) { EmptyView() }
NavigationLink(destination: Text("Third View"), tag: "Third", selection: $selection) { EmptyView() }
Button("Tap to show second") {
self.selection = "Second"
}
Button("Tap to show third") {
self.selection = "Third"
}
}
.navigationBarTitle("Navigation")
}
}
}
Run Code Online (Sandbox Code Playgroud)
您可以使用@environmentobject
注入ContentView()
来处理选择:
class NavigationHelper: ObservableObject {
@Published var selection: String? = nil
}
Run Code Online (Sandbox Code Playgroud)
注入应用程序:
@main
struct YourApp: App {
var body: some Scene {
WindowGroup {
ContentView().environmentObject(NavigationHelper())
}
}
}
Run Code Online (Sandbox Code Playgroud)
并使用它:
struct ContentView: View {
@EnvironmentObject var navigationHelper: NavigationHelper
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: Text("Second View"), tag: "Second", selection: $navigationHelper.selection) { EmptyView() }
NavigationLink(destination: Text("Third View"), tag: "Third", selection: $navigationHelper.selection) { EmptyView() }
Button("Tap to show second") {
self.navigationHelper.selection = "Second"
}
Button("Tap to show third") {
self.navigationHelper.selection = "Third"
}
}
.navigationBarTitle("Navigation")
}
}
}
Run Code Online (Sandbox Code Playgroud)
要返回子导航链接中的内容视图,您只需设置navigationHelper.selection = nil
.
请注意,如果您不想,您甚至不必为后续的子导航链接使用标记和选择——尽管它们没有转到特定导航链接的功能。
小智 10
由于目前 SwiftUI 仍然在后台使用 UINavigationController,因此也可以调用它的popToRootViewController(animated:)
函数。你只需要像这样搜索 UINavigationController 的视图控制器层次结构:
struct NavigationUtil {
static func popToRootView() {
findNavigationController(viewController: UIApplication.shared.windows.filter { $0.isKeyWindow }.first?.rootViewController)?
.popToRootViewController(animated: true)
}
static func findNavigationController(viewController: UIViewController?) -> UINavigationController? {
guard let viewController = viewController else {
return nil
}
if let navigationController = viewController as? UINavigationController {
return navigationController
}
for childViewController in viewController.children {
return findNavigationController(viewController: childViewController)
}
return nil
}
}
Run Code Online (Sandbox Code Playgroud)
并像这样使用它:
struct ContentView: View {
var body: some View {
NavigationView { DummyView(number: 1) }
}
}
struct DummyView: View {
let number: Int
var body: some View {
VStack(spacing: 10) {
Text("This is view \(number)")
NavigationLink(destination: DummyView(number: number + 1)) {
Text("Go to view \(number + 1)")
}
Button(action: { NavigationUtil.popToRootView() }) {
Text("Or go to root view!")
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
小智 9
我想出了一个简单的解决方案来弹出根视图。我正在发送通知,然后监听通知以更改 NavigationView 的 id;这将刷新导航视图。虽然没有动画,但是看起来不错。这是示例:
@main
struct SampleApp: App {
@State private var navigationId = UUID()
var body: some Scene {
WindowGroup {
NavigationView {
Screen1()
}
.id(navigationId)
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("popToRootView"))) { output in
navigationId = UUID()
}
}
}
}
struct Screen1: View {
var body: some View {
VStack {
Text("This is screen 1")
NavigationLink("Show Screen 2", destination: Screen2())
}
}
}
struct Screen2: View {
var body: some View {
VStack {
Text("This is screen 2")
Button("Go to Home") {
NotificationCenter.default.post(name: Notification.Name("popToRootView"), object: nil)
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
小智 7
这是对x0randgat3 答案的更新,适用NavigationViews
于TabView
.
struct NavigationUtil {
static func popToRootView(animated: Bool = false) {
findNavigationController(viewController: UIApplication.shared.connectedScenes.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }.first { $0.isKeyWindow }?.rootViewController)?.popToRootViewController(animated: animated)
}
static func findNavigationController(viewController: UIViewController?) -> UINavigationController? {
guard let viewController = viewController else {
return nil
}
if let navigationController = viewController as? UITabBarController {
return findNavigationController(viewController: navigationController.selectedViewController)
}
if let navigationController = viewController as? UINavigationController {
return navigationController
}
for childViewController in viewController.children {
return findNavigationController(viewController: childViewController)
}
return nil
}
}
Run Code Online (Sandbox Code Playgroud)
我花了最后几个小时来尝试解决相同的问题。据我所知,使用当前的beta 5并没有简单的方法。我发现的唯一方法是非常hacky,但是可以使用。基本上将发布者添加到您的DetailViewA中,这将从DetailViewB触发。在DetailViewB中,关闭视图并通知发布者,他自己将关闭DetailViewA。
struct DetailViewB: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var publisher = PassthroughSubject<Void, Never>()
var body: some View {
VStack {
Text("This is Detail View B.")
Button(action: { self.presentationMode.value.dismiss() } )
{ Text("Pop to Detail View A.") }
Button(action: {
DispatchQueue.main.async {
self.presentationMode.wrappedValue.dismiss()
self.publisher.send()
}
} )
{ Text("Pop two levels to Master View.") }
}
}
}
struct DetailViewA: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var publisher = PassthroughSubject<Void, Never>()
var body: some View {
VStack {
Text("This is Detail View A.")
NavigationLink(destination: DetailViewB(publisher:self.publisher) )
{ Text("Push to Detail View B.") }
Button(action: { self.presentationMode.value.dismiss() } )
{ Text("Pop one level to Master.") }
}
.onReceive(publisher, perform: { _ in
DispatchQueue.main.async {
print("Go Back to Master")
self.presentationMode.wrappedValue.dismiss()
}
})
}
}
Run Code Online (Sandbox Code Playgroud)
[更新]我仍在努力,因为上一个Beta 6仍没有解决方案。
我找到了另一种方法可以返回到根目录,但是这次我丢失了动画,直接进入了根目录。想法是强制刷新根视图,以这种方式导致导航堆栈的清理。
但是最终,只有Apple才能带来适当的解决方案,因为SwiftUI中无法使用导航堆栈的管理。
注意:以下通知提供的简单解决方案适用于iOS而非watchOS,因为watchOS会在2个导航级别后从内存中清除根视图。但是让外部类管理watchOS的状态应该可以正常工作。
struct DetailViewB: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
@State var fullDissmiss:Bool = false
var body: some View {
SGNavigationChildsView(fullDissmiss: self.fullDissmiss){
VStack {
Text("This is Detail View B.")
Button(action: { self.presentationMode.wrappedValue.dismiss() } )
{ Text("Pop to Detail View A.") }
Button(action: {
self.fullDissmiss = true
} )
{ Text("Pop two levels to Master View with SGGoToRoot.") }
}
}
}
}
struct DetailViewA: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
@State var fullDissmiss:Bool = false
var body: some View {
SGNavigationChildsView(fullDissmiss: self.fullDissmiss){
VStack {
Text("This is Detail View A.")
NavigationLink(destination: DetailViewB() )
{ Text("Push to Detail View B.") }
Button(action: { self.presentationMode.wrappedValue.dismiss() } )
{ Text("Pop one level to Master.") }
Button(action: { self.fullDissmiss = true } )
{ Text("Pop one level to Master with SGGoToRoot.") }
}
}
}
}
struct MasterView: View {
var body: some View {
VStack {
Text("This is Master View.")
NavigationLink(destination: DetailViewA() )
{ Text("Push to Detail View A.") }
}
}
}
struct ContentView: View {
var body: some View {
SGRootNavigationView{
MasterView()
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
struct SGRootNavigationView<Content>: View where Content: View {
let cancellable = NotificationCenter.default.publisher(for: Notification.Name("SGGoToRoot"), object: nil)
let content: () -> Content
init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
@State var goToRoot:Bool = false
var body: some View {
return
Group{
if goToRoot == false{
NavigationView {
content()
}
}else{
NavigationView {
content()
}
}
}.onReceive(cancellable, perform: {_ in
DispatchQueue.main.async {
self.goToRoot.toggle()
}
})
}
}
struct SGNavigationChildsView<Content>: View where Content: View {
let notification = Notification(name: Notification.Name("SGGoToRoot"))
var fullDissmiss:Bool{
get{ return false }
set{ if newValue {self.goToRoot()} }
}
let content: () -> Content
init(fullDissmiss:Bool, @ViewBuilder content: @escaping () -> Content) {
self.content = content
self.fullDissmiss = fullDissmiss
}
var body: some View {
return Group{
content()
}
}
func goToRoot(){
NotificationCenter.default.post(self.notification)
}
}
Run Code Online (Sandbox Code Playgroud)
花了一些时间,但我想出了如何在 swiftui 中使用复杂的导航。诀窍是收集视图的所有状态,以判断它们是否显示。
首先定义一个 NavigationController。我添加了 tabview 选项卡的选择和布尔值,表示是否显示特定视图
import SwiftUI
final class NavigationController: ObservableObject {
@Published var selection: Int = 1
@Published var tab1Detail1IsShown = false
@Published var tab1Detail2IsShown = false
@Published var tab2Detail1IsShown = false
@Published var tab2Detail2IsShown = false
}
Run Code Online (Sandbox Code Playgroud)
使用两个选项卡设置 tabview 并将我们的 NavigationController.selection 绑定到 tabview:
import SwiftUI
struct ContentView: View {
@EnvironmentObject var nav: NavigationController
var body: some View {
TabView(selection: self.$nav.selection){
FirstMasterView()
.tabItem {
Text("First")
}
.tag(0)
SecondMasterView()
.tabItem {
Text("Second")
}
.tag(1)
}
}
}
Run Code Online (Sandbox Code Playgroud)
例如,这是一个 navigationStacks
import SwiftUI
struct FirstMasterView: View {
@EnvironmentObject var nav: NavigationController
var body: some View {
NavigationView{
VStack{
NavigationLink(destination: FirstDetailView(), isActive: self.$nav.tab1Detail1IsShown) {
Text("go to first detail")
}
} .navigationBarTitle(Text("First MasterView"))
}
}
}
struct FirstDetailView: View {
@EnvironmentObject var nav: NavigationController
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
VStack(spacing: 20) {
Text("first detail View").font(.title)
NavigationLink(destination: FirstTabLastView(), isActive: self.$nav.tab1Detail2IsShown) {
Text("go to last detail on nav stack")
}
Button(action: {
self.nav.tab2Detail1IsShown = false //true will go directly to detail
self.nav.tab2Detail2IsShown = false
self.nav.selection = 1
}) { Text("Go to second tab")
}
}
//in case of collapsing all the way back
//there is a bug with the environment object
//to go all the way back I have to use the presentationMode
.onReceive(self.nav.$tab1Detail2IsShown, perform: { (out) in
if out == false {
self.presentationMode.wrappedValue.dismiss()
}
})
}
}
struct FirstTabLastView: View {
@EnvironmentObject var nav: NavigationController
var body: some View {
Button(action: {
self.nav.tab1Detail1IsShown = false
self.nav.tab1Detail2IsShown = false
}) {Text("Done and go back to beginning of navigation stack")
}
}
}
Run Code Online (Sandbox Code Playgroud)
我希望我能解释这种方法,它非常面向 SwiftUI 状态。
对我来说,为了实现对 swiftUI 中仍然缺少的导航的完全控制,我只是将 SwiftUI 视图嵌入到UINavigationController
. 里面SceneDelegate
。请注意,我隐藏了导航栏以便使用 NavigationView 作为我的显示。
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
UINavigationBar.appearance().tintColor = .black
let contentView = OnBoardingView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
let hostingVC = UIHostingController(rootView: contentView)
let mainNavVC = UINavigationController(rootViewController: hostingVC)
mainNavVC.navigationBar.isHidden = true
window.rootViewController = mainNavVC
self.window = window
window.makeKeyAndVisible()
}
}
}
Run Code Online (Sandbox Code Playgroud)
然后我创建了这个协议和扩展, HasRootNavigationController
import SwiftUI
import UIKit
protocol HasRootNavigationController {
var rootVC:UINavigationController? { get }
func push<Content:View>(view: Content, animated:Bool)
func setRootNavigation<Content:View>(views:[Content], animated:Bool)
func pop(animated: Bool)
func popToRoot(animated: Bool)
}
extension HasRootNavigationController where Self:View {
var rootVC:UINavigationController? {
guard let scene = UIApplication.shared.connectedScenes.first,
let sceneDelegate = scene as? UIWindowScene,
let rootvc = sceneDelegate.windows.first?.rootViewController
as? UINavigationController else { return nil }
return rootvc
}
func push<Content:View>(view: Content, animated:Bool = true) {
rootVC?.pushViewController(UIHostingController(rootView: view), animated: animated)
}
func setRootNavigation<Content:View>(views: [Content], animated:Bool = true) {
let controllers = views.compactMap { UIHostingController(rootView: $0) }
rootVC?.setViewControllers(controllers, animated: animated)
}
func pop(animated:Bool = true) {
rootVC?.popViewController(animated: animated)
}
func popToRoot(animated: Bool = true) {
rootVC?.popToRootViewController(animated: animated)
}
}
Run Code Online (Sandbox Code Playgroud)
之后在我的 SwiftUI 视图上我使用/实现了HasRootNavigationController
协议和扩展
extension YouSwiftUIView:HasRootNavigationController {
func switchToMainScreen() {
self.setRootNavigation(views: [MainView()])
}
func pushToMainScreen() {
self.push(view: [MainView()])
}
func goBack() {
self.pop()
}
func showTheInitialView() {
self.popToRoot()
}
}
Run Code Online (Sandbox Code Playgroud)
这是我的代码的要点,以防我有一些更新。https://gist.github.com/michaelhenry/945fc63da49e960953b72bbc567458e6
该解决方案基于malhal 的回答,使用Imthath和Florin Odagiu的建议,并需要 Paul Hudson 的 NavigationView 视频为我将所有内容整合在一起。
这个想法很简单。点击时,navigationLink 的 isActive 参数设置为 true。这允许出现第二个视图。您可以使用其他链接来添加更多视图。要返回到根目录,只需将 isActive 设置为 false 即可。第二个视图以及任何其他可能堆积起来的视图都会消失。
import SwiftUI
class Views: ObservableObject {
@Published var stacked = false
}
struct ContentView: View {
@ObservedObject var views = Views()
var body: some View {
NavigationView {
NavigationLink(destination: ContentView2(), isActive: self.$views.stacked) {
Text("Go to View 2") // Tapping this link sets stacked to true
}
.isDetailLink(false)
.navigationBarTitle("ContentView")
}
.environmentObject(views) // Inject a new views instance into the navigation view environment so that it's available to all views presented by the navigation view.
}
}
struct ContentView2: View {
var body: some View {
NavigationLink(destination: ContentView3()) {
Text("Go to View 3")
}
.isDetailLink(false)
.navigationBarTitle("View 2")
}
}
struct ContentView3: View {
@EnvironmentObject var views: Views
var body: some View {
Button("Pop to root") {
self.views.stacked = false // By setting this to false, the second view that was active is no more. Which means, the content view is being shown once again.
}
.navigationBarTitle("View 3")
}
}
Run Code Online (Sandbox Code Playgroud)
归档时间: |
|
查看次数: |
1957 次 |
最近记录: |