SwiftUI: как открыть Root View
Наконец, теперь с бета-версией 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()
}
}
}
33 ответа
Я провел последние часы, чтобы попытаться решить ту же проблему. Насколько я вижу, нелегкий способ сделать это с текущей бета-версией 5. Единственный способ, который я нашел, очень хакерский, но работает. В основном добавьте издателя к вашему 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()
}
})
}
}
[ОБНОВЛЕНИЕ] Я все еще работаю над этим, поскольку на последней бета-версии 6 до сих пор нет решения.
Я нашел другой способ вернуться к корню, но на этот раз я теряю анимацию и иду прямо к корню. Идея заключается в принудительном обновлении корневого представления, что приводит к очистке стека навигации.
Но в конечном итоге только Apple может предложить правильное решение, так как управление навигационным стеком недоступно в SwiftUI.
NB. Простое решение с помощью уведомлений ниже работает на 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)
}
}
Установка модификатора представления isDetailLink
к false
на NavigationLink
это ключ к тому, чтобы заставить работать pop-to-root. isDetailLink
является true
по умолчанию и адаптируется к содержащему его представлению. Например, в альбомной ориентации iPad разделенный вид разделен иisDetailLink
гарантирует, что вид назначения будет показан с правой стороны. НастройкаisDetailLink
к false
следовательно, это означает, что целевое представление всегда будет помещено в стек навигации; таким образом всегда можно снять.
Наряду с настройкой isDetailLink
к false
на 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()
}
}
Определенно, @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()
}
}
ПРИМЕНЕНИЕ:
Добавьте модификатор представления.environment к корневому NavigationView, чтобы установить значение ключа.rootPresentationMode для привязки Bool, используемой для представления первого дочернего представления.
.environment (.rootPresentationMode, self. $isActive)
Добавьте оболочку свойства @Environment к любому представлению, которое захочет вернуться в корневое представление.
@Environment(.rootPresentationMode) private var rootPresentationMode: привязка
Наконец, вызов метода dismiss() для обернутого значения @Environment var приведет к появлению корневого представления.
self.rootPresentationMode.wrappedValue.dismiss()
Я опубликовал полный рабочий пример на GitHub:
Поскольку в настоящее время 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
}
}
И используйте это так:
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!")
}
}
}
}
Дамы и господа, представляем решение Apple для этой самой проблемы. * также представлен вам через HackingWithSwift (который я украл из lol): под программной навигацией
(Проверено на Xcode 12 и iOS 14)
по сути вы используете
tag
и
selection
внутри
navigationlink
чтобы перейти прямо на любую страницу, которую вы хотите.
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")
}
}
}
Вы можете использовать
@environmentobject
введен в
ContentView()
для обработки выбора:
class NavigationHelper: ObservableObject {
@Published var selection: String? = nil
}
ввести в приложение:
@main
struct YourApp: App {
var body: some Scene {
WindowGroup {
ContentView().environmentObject(NavigationHelper())
}
}
}
и используйте это:
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")
}
}
}
Чтобы вернуться к просмотру содержимого в дочерних навигационных ссылках, вы просто устанавливаете
navigationHelper.selection = nil
.
Обратите внимание, что вам даже не нужно использовать тег и выбор для последующих дочерних навигационных ссылок, если вы не хотите - у них не будет функциональности для перехода к этой конкретной навигационной ссылке.
Решение 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()
}
}
Я нашел простое решение, чтобы перейти к корневому представлению. Я отправляю уведомление, а затем слушаю уведомление об изменении идентификатора NavigationView, это обновит NavigationView. Анимации нет, но выглядит неплохо. Вот пример:
@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)
}
}
}
}
Это обновление для
@x0randgat3
ответ, который работает для нескольких
NavigationViews
в пределах
TabView
.
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? 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
}
}
Это заняло некоторое время, но я понял, как использовать сложную навигацию в 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
}
настройка 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)
}
}
}
В качестве примера это один 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")
}
}
}
Я надеюсь, что смогу объяснить подход, который полностью ориентирован на состояние SwiftUI.
Это решение основано на ответе Малхала, использует предложения Имтата и Флорина Одагиу и требует видео Пола Хадсона NavigationView, чтобы собрать все это для меня. Идея очень проста. Для параметра isActive в navigationLink устанавливается значение 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")
}
}
Для меня, чтобы получить полный контроль над навигацией, которая все еще отсутствует в swiftUI, я просто встроил SwiftUI View в 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()
}
}
}
И затем я создал этот протокол и расширение, 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)
}
}
После этого в моем представлении 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()
}
}
вот суть моего кода на случай, если у меня появятся какие-то обновления. https://gist.github.com/michaelhenry/945fc63da49e960953b72bbc567458e6
Спасибо "Малхал" за ваше решение @Binding. Мне не хватало.isDetailLink(false)
модификатор. Что я узнал из вашего кода.
В моем случае я не хочу использовать @Binding при каждом последующем просмотре.
Итак, это мое решение, в котором я использую EnvironmentObject.
Шаг 1. Создайте AppState
ObservableObject
import SwiftUI
import Combine
class AppState: ObservableObject {
@Published var moveToDashboard: Bool = false
}
Шаг 2. Создайте экземпляр AppState
и добавить в contentView
в SceneDelegate
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
let appState = AppState()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView:
contentView
.environmentObject(appState)
)
self.window = window
window.makeKeyAndVisible()
}
}
Шаг 3: Код ContentView.swift
Итак, я обновляю appState
значение последнего представления в стеке, которое использует .onReceive()
Я захватываю в contentView, чтобы обновить isActive
значение false для NavigationLink.
Ключевым моментом здесь является использование .isDetailLink(false)
с помощью NavigationLink. Иначе не получится.
import SwiftUI
import Combine
class AppState: ObservableObject {
@Published var moveToDashboard: Bool = false
}
struct ContentView: View {
@EnvironmentObject var appState: AppState
@State var isView1Active: Bool = false
var body: some View {
NavigationView {
VStack {
Text("Content View")
.font(.headline)
NavigationLink(destination: View1(), isActive: $isView1Active) {
Text("View 1")
.font(.headline)
}
.isDetailLink(false)
}
.onReceive(self.appState.$moveToDashboard) { moveToDashboard in
if moveToDashboard {
print("Move to dashboard: \(moveToDashboard)")
self.isView1Active = false
self.appState.moveToDashboard = false
}
}
}
}
}
// MARK:- View 1
struct View1: View {
var body: some View {
VStack {
Text("View 1")
.font(.headline)
NavigationLink(destination: View2()) {
Text("View 2")
.font(.headline)
}
}
}
}
// MARK:- View 2
struct View2: View {
@EnvironmentObject var appState: AppState
var body: some View {
VStack {
Text("View 2")
.font(.headline)
Button(action: {
self.appState.moveToDashboard = true
}) {
Text("Move to Dashboard")
.font(.headline)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Вот мое решение, работает везде, без зависимости.
let window = UIApplication.shared.connectedScenes
.filter { $0.activationState == .foregroundActive }
.map { $0 as? UIWindowScene }
.compactMap { $0 }
.first?.windows
.filter { $0.isKeyWindow }
.first
let nvc = window?.rootViewController?.children.first as? UINavigationController
nvc?.popToRootViewController(animated: true)
В iOS15 есть простое решение для этого, используя dismiss () и передав dismiss в подпредставление:
struct ContentView: View {
@State private var showingSheet = false
var body: some View {
NavigationView {
Button("show sheet", action: { showingSheet.toggle()})
.navigationTitle("ContentView")
}.sheet(isPresented: $showingSheet) { FirstSheetView() }
}
}
struct FirstSheetView: View {
@Environment(\.dismiss) var dismiss
var body: some View {
NavigationView {
List {
NavigationLink(destination: SecondSheetView(dismiss: _dismiss) ) {
Text("show 2nd Sheet view")
}
NavigationLink(destination: ThirdSheetView(dismiss: _dismiss) ) {
Text("show 3rd Sheet view")
}
Button("cancel", action: {dismiss()} )
} .navigationTitle("1. SheetView")
}
}
}
struct SecondSheetView: View {
@Environment(\.dismiss) var dismiss
var body: some View {
List {
NavigationLink(destination: ThirdSheetView(dismiss: _dismiss) ) {
Text("show 3rd SheetView")
}
Button("cancel", action: {dismiss()} )
} .navigationTitle("2. SheetView")
}
}
struct ThirdSheetView: View {
@Environment(\.dismiss) var dismiss
var body: some View {
List {
Button("cancel", action: {dismiss()} )
} .navigationTitle("3. SheetView")
}
}
Вот мое медленное, анимированное, немного грубое обратное поп-решение с использованием onAppear, действительное для XCode 11 и iOS 13.1:
import SwiftUI
import Combine
struct NestedViewLevel3: View {
@Binding var resetView:Bool
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
VStack {
Spacer()
Text("Level 3")
Spacer()
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Back")
.padding(.horizontal, 15)
.padding(.vertical, 2)
.foregroundColor(Color.white)
.clipped(antialiased: true)
.background(
RoundedRectangle(cornerRadius: 20)
.foregroundColor(Color.blue)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 40, alignment: .center)
)}
Spacer()
Button(action: {
self.$resetView.wrappedValue = true
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Reset")
.padding(.horizontal, 15)
.padding(.vertical, 2)
.foregroundColor(Color.white)
.clipped(antialiased: true)
.background(
RoundedRectangle(cornerRadius: 20)
.foregroundColor(Color.blue)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 40, alignment: .center)
)}
Spacer()
}
.navigationBarBackButtonHidden(false)
.navigationBarTitle("Level 3", displayMode: .inline)
.onAppear(perform: {print("onAppear level 3")})
.onDisappear(perform: {print("onDisappear level 3")})
}
}
struct NestedViewLevel2: View {
@Binding var resetView:Bool
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
VStack {
Spacer()
NavigationLink(destination: NestedViewLevel3(resetView:$resetView)) {
Text("To level 3")
.padding(.horizontal, 15)
.padding(.vertical, 2)
.foregroundColor(Color.white)
.clipped(antialiased: true)
.background(
RoundedRectangle(cornerRadius: 20)
.foregroundColor(Color.gray)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 40, alignment: .center)
)
.shadow(radius: 10)
}
Spacer()
Text("Level 2")
Spacer()
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Back")
.padding(.horizontal, 15)
.padding(.vertical, 2)
.foregroundColor(Color.white)
.clipped(antialiased: true)
.background(
RoundedRectangle(cornerRadius: 20)
.foregroundColor(Color.blue)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 40, alignment: .center)
)}
Spacer()
}
.navigationBarBackButtonHidden(false)
.navigationBarTitle("Level 2", displayMode: .inline)
.onAppear(perform: {
print("onAppear level 2")
if self.$resetView.wrappedValue {
self.presentationMode.wrappedValue.dismiss()
}
})
.onDisappear(perform: {print("onDisappear level 2")})
}
}
struct NestedViewLevel1: View {
@Binding var resetView:Bool
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
VStack {
Spacer()
NavigationLink(destination: NestedViewLevel2(resetView:$resetView)) {
Text("To level 2")
.padding(.horizontal, 15)
.padding(.vertical, 2)
.foregroundColor(Color.white)
.clipped(antialiased: true)
.background(
RoundedRectangle(cornerRadius: 20)
.foregroundColor(Color.gray)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 40, alignment: .center)
)
.shadow(radius: 10)
}
Spacer()
Text("Level 1")
Spacer()
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Back")
.padding(.horizontal, 15)
.padding(.vertical, 2)
.foregroundColor(Color.white)
.clipped(antialiased: true)
.background(
RoundedRectangle(cornerRadius: 20)
.foregroundColor(Color.blue)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 40, alignment: .center)
)}
Spacer()
}
.navigationBarBackButtonHidden(false)
.navigationBarTitle("Level 1", displayMode: .inline)
.onAppear(perform: {
print("onAppear level 1")
if self.$resetView.wrappedValue {
self.presentationMode.wrappedValue.dismiss()
}
})
.onDisappear(perform: {print("onDisappear level 1")})
}
}
struct RootViewLevel0: View {
@Binding var resetView:Bool
var body: some View {
NavigationView {
VStack {
Spacer()
NavigationLink(destination: NestedViewLevel1(resetView:$resetView)) {
Text("To level 1")
.padding(.horizontal, 15)
.padding(.vertical, 2)
.foregroundColor(Color.white)
.clipped(antialiased: true)
.background(
RoundedRectangle(cornerRadius: 20)
.foregroundColor(Color.gray)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 40, alignment: .center)
)
.shadow(radius: 10)
}
//.disabled(false)
//.hidden()
Spacer()
}
}
//.frame(width:UIScreen.main.bounds.width,height: UIScreen.main.bounds.height - 110)
.navigationBarTitle("Root level 0", displayMode: .inline)
.navigationBarBackButtonHidden(false)
.navigationViewStyle(StackNavigationViewStyle())
.onAppear(perform: {
print("onAppear root level 0")
self.resetNavView()
})
.onDisappear(perform: {print("onDisappear root level 0")})
}
func resetNavView(){
print("resetting objects")
self.$resetView.wrappedValue = false
}
}
struct ContentView: View {
@State var resetView = false
var body: some View {
RootViewLevel0(resetView:$resetView)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
NavigationViewKithttps://github.com/fatbobman/NavigationViewKit
import NavigationViewKit
NavigationView {
List(0..<10) { _ in
NavigationLink("abc", destination: DetailView())
}
}
.navigationViewManager(for: "nv1", afterBackDo: {print("back to root") })
в любом представлении в NavigationView
@Environment(\.navigationManager) var nvmanager
Button("back to root view") {
nvmanager.wrappedValue.popToRoot(tag:"nv1"){
print("other back")
}
}
Вы также можете вызвать его через NotificationCenter, не вызывая его в представлении.
let backToRootItem = NavigationViewManager.BackToRootItem(tag: "nv1", animated: false, action: {})
NotificationCenter.default.post(name: .NavigationViewManagerBackToRoot, object: backToRootItem)
Это очень трудно достичь с помощью и
NavigationLink
. Однако, если вы используете библиотеку https://github.com/canopas/UIPilot , которая является крошечной оболочкой вокруг
NavigationView
, выскочить в любой пункт назначения очень просто.
Предположим, у вас есть маршруты
enum AppRoute: Equatable {
case Home
case Detail
case NestedDetail
}
и у вас есть настроенный корневой вид, как показано ниже
struct ContentView: View {
@StateObject var pilot = UIPilot(initial: AppRoute.Home)
var body: some View {
UIPilotHost(pilot) { route in
switch route {
case .Home: return AnyView(HomeView())
case .Detail: return AnyView(DetailView())
case .NestedDetail: return AnyView(NestedDetail())
}
}
}
}
И вы хотите поп к
Home
из
NestedDetail
экран, просто используйте
popTo
функция.
struct NestedDetail: View {
@EnvironmentObject var pilot: UIPilot<AppRoute>
var body: some View {
VStack {
Button("Go to home", action: {
pilot.popTo(.Home) // Pop to home
})
}.navigationTitle("Nested detail")
}
}
Сначала я использовал решение от user899918 , которое было опубликовано
Но я столкнулся с проблемой, когда это решение не сработало в моем случае. Это было связано со случаем, когда корневое представление является начальной точкой для двух и более потоков и в какой-то момент этих потоков у пользователя есть возможность сделать . И в этом случае здесь .это решение не сработало, потому что оно имеет одно общее состояние
@Environment(\.rootPresentationMode) private var rootPresentationMode
Я сделал с дополнительным перечислением
Route
который описывает некоторый конкретный поток, в котором пользователь имеет возможность выполнять
pop to root
Менеджер маршрутов:
final class RouteManager: ObservableObject {
@Published
private var routers: [Int: Route] = [:]
subscript(for route: Route) -> Route? {
get {
routers[route.rawValue]
}
set {
routers[route.rawValue] = route
}
}
func select(_ route: Route) {
routers[route.rawValue] = route
}
func unselect(_ route: Route) {
routers[route.rawValue] = nil
}
}
Маршрут:
enum Route: Int, Hashable {
case signUp
case restorePassword
case orderDetails
}
Применение:
struct ContentView: View {
@EnvironmentObject
var routeManager: RouteManager
var body: some View {
NavigationView {
VStack {
NavigationLink(
destination: SignUp(),
tag: .signUp,
selection: $routeManager[for: .signUp]
) { EmptyView() }.isDetailLink(false)
NavigationLink(
destination: RestorePassword(),
tag: .restorePassword,
selection: $routeManager[for: .restorePassword]
) { EmptyView() }.isDetailLink(false)
Button("Sign Up") {
routeManager.select(.signUp)
}
Button("Restore Password") {
routeManager.select(.restorePassword)
}
}
.navigationBarTitle("Navigation")
.onAppear {
routeManager.unselect(.signUp)
routeManager.unselect(.restorePassword)
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
!! ВАЖНЫЙ !!
Вы должны использовать
unselect
метод
RouteManager
когда пользователь идет вперед к потоку, а затем возвращается, нажав на кнопку «Назад». В этом случае необходимо сбросить состояние нашего менеджера маршрутов для ранее выбранных потоков, чтобы избежать неопределенного (неожиданного) поведения:
.onAppear {
routeManager.unselect(.signUp)
routeManager.unselect(.restorePassword)
}
Полный демонстрационный проект вы можете найти здесь
I found a solution that works fine for me. here is how it works:
in the
ContentView.swift
file:
- define a class, declare an
@EnvironmentObject
ofRootSelection
to record the tag of the current active only in root view. - add a modifier
.isDetailLink(false)
to each that is not a final detail view. - use a file system hierarchy to simulate the
NavigationView
. - this solution works fine when the root view has multiple
NavigationLink
.
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
SubView(folder: rootFolder)
}
}
}
struct SubView: View {
@EnvironmentObject var rootSelection: RootSelection
var folder: Folder
var body: some View {
List(self.folder.documents) { item in
if self.folder.documents.count == 0 {
Text("empty folder")
} else {
if self.folder.id == rootFolder.id {
NavigationLink(item.name, destination: SubView(folder: item as! Folder), tag: item.id, selection: self.$rootSelection.tag)
.isDetailLink(false)
} else {
NavigationLink(item.name, destination: SubView(folder: item as! Folder))
.isDetailLink(false)
}
}
}
.navigationBarTitle(self.folder.name, displayMode: .large)
.listStyle(SidebarListStyle())
.overlay(
Button(action: {
rootSelection.tag = nil
}, label: {
Text("back to root")
})
.disabled(self.folder.id == rootFolder.id)
)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(RootSelection())
}
}
class RootSelection: ObservableObject {
@Published var tag: UUID? = nil
}
class Document: Identifiable {
let id = UUID()
var name: String
init(name: String) {
self.name = name
}
}
class File: Document {}
class Folder: Document {
var documents: [Document]
init(name: String, documents: [Document]) {
self.documents = documents
super.init(name: name)
}
}
let rootFolder = Folder(name: "root", documents: [
Folder(name: "folder1", documents: [
Folder(name: "folder1.1", documents: []),
Folder(name: "folder1.2", documents: []),
]),
Folder(name: "folder2", documents: [
Folder(name: "folder2.1", documents: []),
Folder(name: "folder2.2", documents: []),
])
])
.environmentObject(RootSelection())
is required for the
ContentView()
object in
xxxApp.swift
file
import SwiftUI
@main
struct DraftApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(RootSelection())
}
}
}
Недавно я создал проект с открытым исходным кодом под названием swiftui-navigation-stack
(https://github.com/biobeats/swiftui-navigation-stack). Это альтернативный стек навигации для SwiftUI. Взгляните на README, чтобы узнать все подробности, он действительно прост в использовании.
Прежде всего, если вы хотите перемещаться между экранами (например, в полноэкранном режиме), определите свой собственный простой Screen
Посмотреть:
struct Screen<Content>: View where Content: View {
let myAppBackgroundColour = Color.white
let content: () -> Content
var body: some View {
ZStack {
myAppBackgroundColour.edgesIgnoringSafeArea(.all)
content()
}
}
}
Затем вставьте свой корень в NavigationStackView
(как и со стандартным NavigationView
):
struct RootView: View {
var body: some View {
NavigationStackView {
Homepage()
}
}
}
Теперь давайте создадим пару дочерних представлений, чтобы показать вам основное поведение:
struct Homepage: View {
var body: some View {
Screen {
PushView(destination: FirstChild()) {
Text("PUSH FORWARD")
}
}
}
}
struct FirstChild: View {
var body: some View {
Screen {
VStack {
PopView {
Text("JUST POP")
}
PushView(destination: SecondChild()) {
Text("PUSH FORWARD")
}
}
}
}
}
struct SecondChild: View {
var body: some View {
Screen {
VStack {
PopView {
Text("JUST POP")
}
PopView(destination: .root) {
Text("POP TO ROOT")
}
}
}
}
}
Вы можете использовать PushView
а также PopView
перемещаться вперед и назад. Конечно, ваше представление содержимого внутриSceneDelegate
должно быть:
// Create the SwiftUI view that provides the window contents.
let contentView = RootView()
Результат:
Ответ @malhal определенно правильный. Я сделал оболочку, которая позволяет мне применять любые модификаторы, которые мне нужны, кроме
isDetailLink(false)
один и записывать любые данные, которые мне нужны. В частности, он фиксирует
isActive
привязка или
tag
привязка, чтобы я мог сбросить их, когда я хочу перейти к любому представлению, объявленному корнем. Параметр
isRoot = true
сохранит привязку для этого представления, а
dismiss
Параметр принимает необязательное закрытие на случай, если вам нужно что-то сделать, когда произойдет всплывающее окно. Я скопировал базовые подписи из SwiftUI
NavigationLink
s для простой навигации на основе логических значений или тегов, чтобы можно было легко редактировать существующие варианты использования. При необходимости должно быть просто добавить другие. Обертка выглядит так:
struct NavigationStackLink<Label, Destination> : View where Label : View, Destination : View {
var isActive: Binding<Bool>? // Optionality implies whether tag or Bool binding is used
var isRoot: Bool = false
let link: NavigationLink<Label,Destination>
private var dismisser: () -> Void = {}
/// Wraps [NavigationLink](https://developer.apple.com/documentation/swiftui/navigationlink/init(isactive:destination:label:))
/// `init(isActive: Binding<Bool>, destination: () -> Destination, label: () -> Label)`
/// - Parameters:
/// - isActive: A Boolean binding controlling the presentation state of the destination
/// - isRoot: Indicate if this is the root view. Used to pop to root level. Default `false`
/// - dismiss: A closure that is called when the link destination is about to be dismissed
/// - destination: The link destination view
/// - label: The links label
init(isActive: Binding<Bool>, isRoot : Bool = false, dismiss: @escaping () -> Void = {}, @ViewBuilder destination: @escaping () -> Destination, @ViewBuilder label: @escaping () -> Label) {
self.isActive = isActive
self.isRoot = isRoot
self.link = NavigationLink(isActive: isActive, destination: destination, label: label)
self.dismisser = dismiss
}
/// Wraps [NavigationLink ](https://developer.apple.com/documentation/swiftui/navigationlink/init(tag:selection:destination:label:))
init<V>(tag: V, selection: Binding<V?>, isRoot : Bool = false, dismiss: @escaping () -> Void = {}, @ViewBuilder destination: @escaping () -> Destination, @ViewBuilder label: @escaping () -> Label) where V : Hashable
{
self.isRoot = isRoot
self.link = NavigationLink(tag: tag, selection: selection, destination: destination, label: label)
self.dismisser = dismiss
self.isActive = Binding (get: {
selection.wrappedValue == tag
}, set: { newValue in
if newValue {
selection.wrappedValue = tag
} else {
selection.wrappedValue = nil
}
})
}
// Make sure you inject your external store into your view hierarchy
@EnvironmentObject var viewRouter: ViewRouter
var body: some View {
// Store whatever you need to in your external object
if isRoot {
viewRouter.root = isActive
}
viewRouter.dismissals.append(self.dismisser)
// Return the link with whatever modification you need
return link
.isDetailLink(false)
}
}
The
ViewRouter
может быть все, что вам нужно. я использовал
ObservableObject
с намерением в конечном итоге добавить некоторые
Published
значения для более сложных манипуляций со стеком в будущем:
класс ViewRouter: ObservableObject {
var root: Binding<Bool>?
typealias Dismiss = () -> Void
var dismissals : [Dismiss] = []
func popToRoot() {
dismissals.forEach { dismiss in
dismiss()
}
dismissals = []
root?.wrappedValue = false
}
}
Я пока не нашел решения в SwiftUI, но нашел эту библиотеку: https://github.com/knoggl/CleanUI
Используя класс CUNavigation , я могу получить именно тот шаблон навигации, который мне нужен.
Пример из readme библиотеки:
NavigationView {
Button(action: {
CUNavigation.pushToSwiftUiView(YOUR_VIEW_HERE)
}){
Text("Push To SwiftUI View")
}
Button(action: {
CUNavigation.popToRootView()
}){
Text("Pop to the Root View")
}
Button(action: {
CUNavigation.pushBottomSheet(YOUR_VIEW_HERE)
}){
Text("Push to a Botton-Sheet")
}
}
Ответ от user259521 действительно выручил, но в моей ситуации мне нужна была функциональность при нажатии каждой кнопки перед навигацией. Если вы находитесь в той же лодке, попробуйте этот код!
// ContentView.swift
// Navigation View Buttons
//
// Created by Jarren Campos on 9/10/22.
//
import SwiftUI
struct ContentView: View {
var body: some View{
VStack{
ContentView1()
}
}
}
struct ContentView1: View {
@State var isActive : Bool = false
var body: some View {
NavigationView {
VStack{
Button {
isActive = true
} label: {
Text("To 2")
}
}
.background{
NavigationLink(
destination: ContentView2(rootIsActive: self.$isActive),
isActive: self.$isActive) {}
.isDetailLink(false)
}
.navigationBarTitle("One")
}
}
}
struct ContentView2: View {
@Binding var rootIsActive : Bool
@State var toThirdView: Bool = false
var body: some View {
VStack{
Button {
toThirdView = true
} label: {
Text("to 3")
}
}
.background{
NavigationLink(isActive: $toThirdView) {
ContentView3(shouldPopToRootView: self.$rootIsActive)
} label: {}
.isDetailLink(false)
}
.navigationBarTitle("Two")
}
}
struct ContentView3: View {
@Binding var shouldPopToRootView : Bool
var body: some View {
VStack {
Text("Hello, World #3!")
Button {
self.shouldPopToRootView = false
} label: {
Text("Pop to root")
}
}
.navigationBarTitle("Three")
}
}
Подробности
- Версия Xcode 13.2.1 (13C100), Swift 5.5
Решение
Связанный список
https://github.com/raywenderlich/swift-algorithm-club/blob/master/Linked%20List/LinkedList.swift
Стек навигации
import SwiftUI
import Combine
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MARK: Custom NavigationLink
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
final class CustomNavigationLinkViewModel<CustomViewID>: ObservableObject where CustomViewID: Equatable {
private weak var navigationStack: NavigationStack<CustomViewID>?
/// `viewId` is used to find a `CustomNavigationLinkViewModel` in the `NavigationStack`
let viewId = UUID().uuidString
/// `customId` is used to mark a `CustomNavigationLink` in the `NavigationStack`. This is kind of external id.
/// In `NavigationStack` we always prefer to use `viewId`. But from time to time we need to implement `pop several views`
/// and that is the purpose of the `customId`
/// Developer can just create a link with `customId` e.g. `navigationStack.navigationLink(customId: "123") { .. }`
/// And to pop directly to view `"123"` should use `navigationStack.popToLast(customId: "123")`
let customId: CustomViewID?
@Published var isActive = false {
didSet { navigationStack?.updated(linkViewModel: self) }
}
init (navigationStack: NavigationStack<CustomViewID>, customId: CustomViewID? = nil) {
self.navigationStack = navigationStack
self.customId = customId
}
}
extension CustomNavigationLinkViewModel: Equatable {
static func == (lhs: CustomNavigationLinkViewModel, rhs: CustomNavigationLinkViewModel) -> Bool {
lhs.viewId == rhs.viewId && lhs.customId == rhs.customId
}
}
struct CustomNavigationLink<Label, Destination, CustomViewID>: View where Label: View, Destination: View, CustomViewID: Equatable {
/// Link `ViewModel` where all states are stored
@StateObject var viewModel: CustomNavigationLinkViewModel<CustomViewID>
let destination: () -> Destination
let label: () -> Label
var body: some View {
NavigationLink(isActive: $viewModel.isActive, destination: destination, label: label)
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MARK: NavigationStack
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
class NavigationStack<CustomViewID>: ObservableObject where CustomViewID: Equatable {
typealias Link = WeakReference<CustomNavigationLinkViewModel<CustomViewID>>
private var linkedList = LinkedList<Link>()
func navigationLink<Label, Destination>(customId: CustomViewID? = nil,
@ViewBuilder destination: @escaping () -> Destination,
@ViewBuilder label: @escaping () -> Label)
-> some View where Label: View, Destination: View {
createNavigationLink(customId: customId, destination: destination, label: label)
}
private func createNavigationLink<Label, Destination>(customId: CustomViewID? = nil,
@ViewBuilder destination: @escaping () -> Destination,
@ViewBuilder label: @escaping () -> Label)
-> CustomNavigationLink<Label, Destination, CustomViewID> where Label: View, Destination: View {
.init(viewModel: CustomNavigationLinkViewModel(navigationStack: self, customId: customId),
destination: destination,
label: label)
}
}
// MARK: Nested Types
extension NavigationStack {
/// To avoid retain cycle it is important to store weak reference to the `CustomNavigationLinkViewModel`
final class WeakReference<T> where T: AnyObject {
private(set) weak var weakReference: T?
init(value: T) { self.weakReference = value }
deinit { print("deinited WeakReference") }
}
}
// MARK: Searching
extension NavigationStack {
private func last(where condition: (Link) -> Bool) -> LinkedList<Link>.Node? {
var node = linkedList.last
while(node != nil) {
if let node = node, condition(node.value) {
return node
}
node = node?.previous
}
return nil
}
}
// MARK: Binding
extension NavigationStack {
fileprivate func updated(linkViewModel: CustomNavigationLinkViewModel<CustomViewID>) {
guard linkViewModel.isActive else {
switch linkedList.head?.value.weakReference {
case nil: break
case linkViewModel: linkedList.removeAll()
default:
last (where: { $0.weakReference === linkViewModel })?.previous?.next = nil
}
return
}
linkedList.append(WeakReference(value: linkViewModel))
}
}
// MARK: pop functionality
extension NavigationStack {
func popToRoot() {
linkedList.head?.value.weakReference?.isActive = false
}
func pop() {
linkedList.last?.value.weakReference?.isActive = false
}
func popToLast(customId: CustomViewID) {
last (where: { $0.weakReference?.customId == customId })?.value.weakReference?.isActive = false
}
}
#if DEBUG
extension NavigationStack {
var isEmpty: Bool { linkedList.isEmpty }
var count: Int { linkedList.count }
func testCreateNavigationLink<Label, Destination>(viewModel: CustomNavigationLinkViewModel<CustomViewID>,
@ViewBuilder destination: @escaping () -> Destination,
@ViewBuilder label: @escaping () -> Label)
-> CustomNavigationLink<Label, Destination, CustomViewID> where Label: View, Destination: View {
.init(viewModel: viewModel, destination: destination, label: label)
}
}
#endif
Использование (краткий пример)
Создать навигационную ссылку:
struct Page: View {
@EnvironmentObject var navigationStack: NavigationStack<String>
var body: some View {
navigationStack.navigationLink {
NextView(...)
} label: {
Text("Next page")
}
}
}
Поп-функциональность
struct Page: View {
@EnvironmentObject var navigationStack: NavigationStack<String>
var body: some View {
Button("Pop") {
navigationStack.pop()
}
Button("Pop to Page 1") {
navigationStack.popToLast(customId: "1")
}
Button("Pop to root") {
navigationStack.popToRoot()
}
}
}
Использование (полный пример)
import SwiftUI
struct ContentView: View {
var body: some View {
TabView {
addTab(title: "Tab 1", systemImageName: "house")
addTab(title: "Tab 2", systemImageName: "bookmark")
}
}
func addTab(title: String, systemImageName: String) -> some View {
NavigationView {
RootPage(title: "\(title) home")
.navigationBarTitle(title)
}
.environmentObject(NavigationStack<String>())
.navigationViewStyle(StackNavigationViewStyle())
.tabItem {
Image(systemName: systemImageName)
Text(title)
}
}
}
struct RootPage: View {
let title: String
var body: some View {
SimplePage(title: title, pageCount: 0)
}
}
struct SimplePage: View {
@EnvironmentObject var navigationStack: NavigationStack<String>
var title: String
var pageCount: Int
var body: some View {
VStack {
navigationStack.navigationLink(customId: "\(pageCount)") {
// router.navigationLink {
SimplePage(title: "Page: \(pageCount + 1)", pageCount: pageCount + 1)
} label: {
Text("Next page")
}
Button("Pop") {
navigationStack.pop()
}
Button("Pop to Page 1") {
navigationStack.popToLast(customId: "1")
}
Button("Pop to root") {
navigationStack.popToRoot()
}
}
.navigationTitle(title)
}
}
Некоторые модульные тесты
@testable import SwiftUIPop
import XCTest
import SwiftUI
import Combine
class SwiftUIPopTests: XCTestCase {
typealias CustomLinkID = String
typealias Stack = NavigationStack<CustomLinkID>
private let stack = Stack()
}
// MARK: Empty Navigation Stack
extension SwiftUIPopTests {
func testNoCrashOnPopToRootOnEmptyStack() {
XCTAssertTrue(stack.isEmpty)
stack.popToRoot()
}
func testNoCrashOnPopToLastOnEmptyStack() {
XCTAssertTrue(stack.isEmpty)
stack.popToLast(customId: "123")
}
func testNoCrashOnPopOnEmptyStack() {
XCTAssertTrue(stack.isEmpty)
stack.pop()
}
}
// MARK: expectation functions
private extension SwiftUIPopTests {
func navigationStackShould(beEmpty: Bool) {
if beEmpty {
XCTAssertTrue(stack.isEmpty, "Navigation Stack should be empty")
} else {
XCTAssertFalse(stack.isEmpty, "Navigation Stack should not be empty")
}
}
}
// MARK: Data / model generators
private extension SwiftUIPopTests {
func createNavigationLink(viewModel: CustomNavigationLinkViewModel<CustomLinkID>, stack: Stack)
-> CustomNavigationLink<EmptyView, EmptyView, CustomLinkID> {
stack.testCreateNavigationLink(viewModel: viewModel) {
EmptyView()
} label: {
EmptyView()
}
}
func createNavigationLinkViewModel(customId: CustomLinkID? = nil) -> CustomNavigationLinkViewModel<CustomLinkID> {
.init(navigationStack: stack, customId: customId)
}
}
// MARK: test `isActive` changing from `true` to `false` on `pop`
extension SwiftUIPopTests {
private func isActiveChangeOnPop(customId: String? = nil,
popAction: (Stack) -> Void,
file: StaticString = #file,
line: UInt = #line) {
navigationStackShould(beEmpty: true)
let expec = expectation(description: "Wait for viewModel.isActive changing")
var canalables = Set<AnyCancellable>()
let viewModel = createNavigationLinkViewModel(customId: customId)
let navigationLink = createNavigationLink(viewModel: viewModel, stack: stack)
navigationLink.viewModel.isActive = true
navigationLink.viewModel.$isActive.dropFirst().sink { value in
expec.fulfill()
}.store(in: &canalables)
navigationStackShould(beEmpty: false)
popAction(stack)
waitForExpectations(timeout: 2)
navigationStackShould(beEmpty: true)
}
func testIsActiveChangeOnPop() {
isActiveChangeOnPop { $0.pop() }
}
func testIsActiveChangeOnPopToRoot() {
isActiveChangeOnPop { $0.popToRoot() }
}
func testIsActiveChangeOnPopToLast() {
let customId = "1234"
isActiveChangeOnPop(customId: customId) { $0.popToLast(customId: customId) }
}
func testIsActiveChangeOnPopToLast2() {
navigationStackShould(beEmpty: true)
let expec = expectation(description: "Wait")
var canalables = Set<AnyCancellable>()
let viewModel = createNavigationLinkViewModel(customId: "123")
let navigationLink = createNavigationLink(viewModel: viewModel, stack: stack)
navigationLink.viewModel.isActive = true
navigationLink.viewModel.$isActive.dropFirst().sink { value in
expec.fulfill()
}.store(in: &canalables)
navigationStackShould(beEmpty: false)
stack.popToLast(customId: "1234")
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
expec.fulfill()
}
waitForExpectations(timeout: 3)
navigationStackShould(beEmpty: false)
}
}
// MARK: Check that changing `CustomNavigationLinkViewModel.isActive` will update `Navigation Stack`
extension SwiftUIPopTests {
// Add and remove view to the empty stack
private func isActiveChangeUpdatesNavigationStack1(createLink: (Stack) -> CustomNavigationLink<EmptyView, EmptyView, String>) {
navigationStackShould(beEmpty: true)
let navigationLink = createLink(stack)
navigationStackShould(beEmpty: true)
navigationLink.viewModel.isActive = true
navigationStackShould(beEmpty: false)
navigationLink.viewModel.isActive = false
navigationStackShould(beEmpty: true)
}
func testIsActiveChangeUpdatesNavigationStack1() {
isActiveChangeUpdatesNavigationStack1 { stack in
let viewModel = createNavigationLinkViewModel()
return createNavigationLink(viewModel: viewModel, stack: stack)
}
}
func testIsActiveChangeUpdatesNavigationStack2() {
isActiveChangeUpdatesNavigationStack1 { stack in
let viewModel = createNavigationLinkViewModel(customId: "123")
return createNavigationLink(viewModel: viewModel, stack: stack)
}
}
// Add and remove view to the non-empty stack
private func isActiveChangeUpdatesNavigationStack2(createLink: (Stack) -> CustomNavigationLink<EmptyView, EmptyView, String>) {
navigationStackShould(beEmpty: true)
let viewModel1 = createNavigationLinkViewModel()
let navigationLink1 = createNavigationLink(viewModel: viewModel1, stack: stack)
navigationLink1.viewModel.isActive = true
navigationStackShould(beEmpty: false)
XCTAssertEqual(stack.count, 1, "Navigation Stack Should contains only one link")
let navigationLink2 = createLink(stack)
navigationLink2.viewModel.isActive = true
navigationStackShould(beEmpty: false)
navigationLink2.viewModel.isActive = false
XCTAssertEqual(stack.count, 1, "Navigation Stack Should contains only one link")
}
func testIsActiveChangeUpdatesNavigationStack3() {
isActiveChangeUpdatesNavigationStack2 { stack in
let viewModel = createNavigationLinkViewModel()
return createNavigationLink(viewModel: viewModel, stack: stack)
}
}
func testIsActiveChangeUpdatesNavigationStack4() {
isActiveChangeUpdatesNavigationStack2 { stack in
let viewModel = createNavigationLinkViewModel(customId: "123")
return createNavigationLink(viewModel: viewModel, stack: stack)
}
}
}
Я придумал другую технику, которая работает, но она все еще кажется странной. Это также все еще оживляет отклонение обоих экранов, но это немного чище. Вы можете либо A) передать закрытие на последующие подробные экраны, либо B) передать подробности B presentationMode
детали. Оба из них требуют удаления детали B, а затем задерживают на короткое время, чтобы деталь A снова появлялась на экране, прежде чем пытаться отклонить детали A.
let minDelay = TimeInterval(0.001)
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink("Push Detail A", destination: DetailViewA())
}.navigationBarTitle("Root View")
}
}
}
struct DetailViewA: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
VStack {
Spacer()
NavigationLink("Push Detail With Closure",
destination: DetailViewWithClosure(dismissParent: { self.dismiss() }))
Spacer()
NavigationLink("Push Detail with Parent Binding",
destination: DetailViewWithParentBinding(parentPresentationMode: self.presentationMode))
Spacer()
}.navigationBarTitle("Detail A")
}
func dismiss() {
print ("Detail View A dismissing self.")
presentationMode.wrappedValue.dismiss()
}
}
struct DetailViewWithClosure: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
@State var dismissParent: () -> Void
var body: some View {
VStack {
Button("Pop Both Details") { self.popParent() }
}.navigationBarTitle("Detail With Closure")
}
func popParent() {
presentationMode.wrappedValue.dismiss()
DispatchQueue.main.asyncAfter(deadline: .now() + minDelay) { self.dismissParent() }
}
}
struct DetailViewWithParentBinding: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
@Binding var parentPresentationMode: PresentationMode
var body: some View {
VStack {
Button("Pop Both Details") { self.popParent() }
}.navigationBarTitle("Detail With Binding")
}
func popParent() {
presentationMode.wrappedValue.dismiss()
DispatchQueue.main.asyncAfter(deadline: .now() + minDelay) { self.parentPresentationMode.dismiss() }
}
}
Чем больше я думаю о том, как работает SwiftUI и как все устроено, тем меньше я думаю, что Apple предоставит что-то эквивалентное popToRootViewController
или другие прямые изменения в стеке навигации. Он бросает вызов тому, как SwiftUI создает структуры представлений, потому что он позволяет дочернему представлению достигать состояния родителя и манипулировать им. Именно это и делают эти подходы, но они делают это явно и открыто. DetailViewA
не может создать ни одно из представлений назначения, не предоставив доступ к своему собственному состоянию, то есть автору необходимо продумать последствия предоставления указанного доступа.
Вот общий подход к сложной навигации, который сочетает в себе многие подходы, описанные здесь. Этот шаблон полезен, если у вас много потоков, которые нужно возвращать в корень, а не только один.
Сначала настройте среду ObservableObject и для удобства чтения используйте перечисление для ввода ваших представлений.
class ActiveView : ObservableObject {
@Published var selection: AppView? = nil
}
enum AppView : Comparable {
case Main, Screen_11, Screen_12, Screen_21, Screen_22
}
[...]
let activeView = ActiveView()
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(activeView))
В основном ContentView используйте кнопки с NavigationLink на EmptyView(). Мы делаем это, чтобы использовать параметр isActive в NavigationLink вместо тега и выделения. Screen_11 в основном представлении должен оставаться активным на Screen_12, и, наоборот, Screen_21 должен оставаться активным с Screen_22, иначе представления будут всплывать. Не забудьте установить для isDetailLink значение false.
struct ContentView: View {
@EnvironmentObject private var activeView: ActiveView
var body: some View {
NavigationView {
VStack {
// These buttons navigate by setting the environment variable.
Button(action: { self.activeView.selection = AppView.Screen_1.1}) {
Text("Navigate to Screen 1.1")
}
Button(action: { self.activeView.selection = AppView.Screen_2.1}) {
Text("Navigate to Screen 2.1")
}
// These are the navigation link bound to empty views so invisible
NavigationLink(
destination: Screen_11(),
isActive: orBinding(b: self.$activeView.selection, value1: AppView.Screen_11, value2: AppView.Screen_12)) {
EmptyView()
}.isDetailLink(false)
NavigationLink(
destination: Screen_21(),
isActive: orBinding(b: self.$activeView.selection, value1: AppView.Screen_21, value2: AppView.Screen_22)) {
EmptyView()
}.isDetailLink(false)
}
}
}
Вы можете использовать тот же шаблон на Screen_11, чтобы перейти к Screen_12.
Теперь прорыв в этой сложной навигации - orBinding. Это позволяет стеку представлений в потоке навигации оставаться активным. Независимо от того, находитесь ли вы на Screen_11 или Screen_12, вам необходимо, чтобы NavigationLink(Screen_11) оставался активным.
// This function create a new Binding<Bool> compatible with NavigationLink.isActive
func orBinding<T:Comparable>(b: Binding<T?>, value1: T, value2: T) -> Binding<Bool> {
return Binding<Bool>(
get: {
return (b.wrappedValue == value1) || (b.wrappedValue == value2)
},
set: { newValue in } // don't care the set
)
}
Элементарно. Достаточно в корневом представлении (куда вы хотите вернуться) использовать NavigationLink с конструктором isActive. В последнем представлении переключитесь на переменную FALSE, управляющую параметром isActive.
В Swift версии 5.5 использование .isDetaillink(false) необязательно.
Вы можете использовать какой-нибудь общий класс, как в примере, или передать эту переменную вниз по иерархии VIEW через привязку. Используйте так, как вам удобнее.
class ViewModel: ObservableObject {
@Published var isActivate = false
}
@main
struct TestPopToRootApp: App {
let vm = ViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(vm)
}
}
}
struct ContentView: View {
@EnvironmentObject var vm: ViewModel
var body: some View {
NavigationView {
NavigationLink("Go to view2", destination: NavView2(), isActive: $vm.isActivate)
.navigationTitle(Text("Root view"))
}
}
}
struct NavView2: View {
var body: some View {
NavigationLink("Go to view3", destination: NavView3())
.navigationTitle(Text("view2"))
}
}
struct NavView3: View {
@EnvironmentObject var vm: ViewModel
var body: some View {
Button {
vm.isActivate = false
} label: {
Text("Back to root")
}
.navigationTitle(Text("view3"))
}
}
Я пробовал использовать метод малхала. Но в моем случае на одной странице 2 ссылки. Вот почему я должен определить 2 переменные типа bool. Это было очень запутанно. После некоторого исследования я нашел эту ссылку на YouTube. Я использовал показанный там метод. Итак, я использовал теги в NavigationLink, и они очень полезны.
Для меня это сработало
guard let firstScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
return
}
guard let firstWindow = firstScene.windows.first else {
return
}
firstWindow.rootViewController = UIHostingController(rootView: YourSwiftUIRootView())
firstWindow.makeKeyAndVisible()
Я создал решение, которое «просто работает». Очень доволен этим. Чтобы использовать мои волшебные решения, вам нужно сделать всего несколько шагов.
Он начинается с использования rootPresentationMode, который используется в другом месте в этом потоке. Добавьте этот код:
// Create a custom environment key
struct RootPresentationModeKey: EnvironmentKey {
static let defaultValue: Binding<RootPresentationMode> = .constant(RootPresentationMode())
}
extension EnvironmentValues {
var rootPresentationMode: Binding<RootPresentationMode> {
get { self[RootPresentationModeKey.self] }
set { self[RootPresentationModeKey.self] = newValue }
}
}
typealias RootPresentationMode = Bool
extension RootPresentationMode: Equatable {
mutating func dismiss() {
toggle()
}
}
Далее идет магия. Он имеет два шага.
- Создайте модификатор представления, который отслеживает изменения в
rootPresentationMode
переменная.
struct WithRoot: ViewModifier {
@Environment(\.rootPresentationMode) private var rootPresentationMode
@Binding var rootBinding: Bool
func body(content: Content) -> some View {
content
.onChange(of: rootBinding) { newValue in
// we only care if it's set to true
if newValue {
rootPresentationMode.wrappedValue = true
}
}
.onChange(of: rootPresentationMode.wrappedValue) { newValue in
// we only care if it's set to false
if !newValue {
rootBinding = false
}
}
}
}
extension View {
func withRoot(rootBinding: Binding<Bool>) -> some View {
modifier(WithRoot(rootBinding: rootBinding))
}
}
- Добавить
isPresented
ко всем навигационным представлениям
struct ContentView: View {
// this seems.. unimportant, but it's crucial. This variable
// lets us pop back to the root view from anywhere by adding
// a withRoot() modifier
// It's only used indirectly by the withRoot() modifier.
@State private var isPresented = false
var body: some View {
NavigationView {
MyMoneyMakingApp()
}
// rootPresentationMode MUST be set on a NavigationView to be
// accessible from everywhere
.environment(\.rootPresentationMode, $isPresented)
}
Чтобы использовать его в (любых) подпредставлениях, все, что вам нужно сделать, это
struct MyMoneyMakingApp: View {
@State private var isActive = false
var body: some View {
VStack {
NavigationLink(destination: ADeepDeepLink(), isActive: $isActive) {
Text("go deep")
}
}
.withRoot(rootBinding: $isActive)
}
}
struct ADeepDeepLink: View {
@Environment(\.rootPresentationMode) private var rootPresentationMode
var body: some View {
VStack {
NavigationLink(destination: ADeepDeepLink()) {
Text("go deeper")
}
Button(action: {
rootPresentationMode.wrappedValue.dismiss()
}) {
Text("pop to root")
}
}
}
}