Swift를 사용하여 루트 보기로 팝업하려면 어떻게 해야 합니까?UI?
마지막으로 베타 5에서 상위 보기로 프로그래밍 방식으로 팝업할 수 있습니다.그러나 내 앱에는 여러 단계 과정을 마치고 처음으로 돌아가는 "저장" 버튼이 있는 곳이 몇 군데 있습니다.UIKit에서는 popToRootViewController()를 사용하고 있습니다만, Swift에서도 같은 방법을 찾을 수 없습니다.UI.
다음은 제가 달성하려는 패턴의 간단한 예입니다.
어떻게 해야 하죠?
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()
}
}
}
설정 " " "isDetailLink
로로 합니다.false
NavigationLink
pop-to-root로 하기 입니다. isDetailLink
true
는 기본적으로 포함되어 있는 보기에 맞게 조정됩니다.되고 "iPad"는 "Split view"와 "Split view는 "Split view"와 "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가 지적한 바와 같이 훨씬 더 좋은 방법이다.
다음은 이전 View로 팝업하는 Apple의 게시된 ofcept() 메서드를 본뜬 또 다른 접근법입니다.
환경에 대한 확장을 정의합니다.
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(\.rootPresentationMode, self.$isPresented)
NavigationView
서, snowledge.isPresented
Bool
첫 번째 아이 뷰를 표시하는 데 사용됩니다." " " 를 합니다.
.navigationViewStyle(StackNavigationViewStyle())
NavigationView
「」를 추가합니다..isDetailLink(false)
NavigationLink
첫 번째 어린이 보기를 위해.@Environment(\.rootPresentationMode) private var rootPresentationMode
팝을 수행해야 하는 모든 하위 보기로 이동합니다.'아까부터'를 합니다.
self.rootPresentationMode.wrappedValue.dismiss()
해당 하위 보기에서 루트 보기로 이동합니다.
Swift(Swift)부터 UI를 UINavigation Controller로 .UINavigation Controller(UINavigation Controller)입니다.popToRootViewController(animated:)
.UINavigation Controller는 다음과 같습니다.
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의 솔루션을 소개합니다.
또한 Hacking With Swift(이걸 훔쳤다는 뜻)를 통해 프로그래밍 방식의 네비게이션 아래 다음과 같이 표시되었습니다.
(Xcode 12 및 iOS 14에서 테스트 완료)
으로는 ★★★★★★★★★★★★★★★★★★★★★★★.tag
★★★★★★★★★★★★★★★★★」selection
에 inside inside inside 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
injected injected injected into 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
.
원하지 않을 경우 태그와 선택 항목을 후속 하위 탐색 링크에 사용할 필요가 없습니다. 단, 특정 탐색 링크로 이동하는 기능은 없습니다.
현재 베타 5에서는 쉽게 할 수 있는 방법이 없습니다.내가 찾은 유일한 방법은 매우 해킹하는 것이지만, 효과가 있다.
기본적으로 DetailViewB에서 트리거되는 퍼블리셔를 DetailViewA에 추가합니다.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에는 아직 해결책이 없습니다.
루트로 돌아갈 수 있는 다른 방법을 찾았는데 이번에는 애니메이션을 잃고 루트로 직행하고 있어요.루트 뷰를 강제로 새로 고치면 네비게이션스택이 정리됩니다.
그러나 Swift에서는 내비게이션 스택 관리를 이용할 수 없기 때문에 궁극적으로 애플만이 적절한 솔루션을 제공할 수 있었다.UI.
주의: 아래 알림에 의한 심플한 솔루션은 watch OS가 아닌 iOS 상에서 동작합니다.OS는 2개의 네비게이션레벨이 끝나면 메모리에서 루트뷰를 클리어합니다.하지만 감시 대상 상태를 관리하는 외부 클래스가 있습니다.OS는 정상적으로 동작합니다.
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)
}
}
루트 뷰로 바로 이동할 수 있는 간단한 해결책을 찾아냈어요.알림을 보내고 Navigation View의 ID를 변경하라는 알림을 수신합니다. 그러면 Navigation View가 새로 고쳐집니다.애니메이션은 없지만 보기 좋다.다음은 예를 제시하겠습니다.
@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)
}
}
}
}
나는 스위프트에서 복잡한 내비게이션을 사용하는 방법을 알아냈다.UI. 뷰의 모든 상태를 수집하여 표시 여부를 확인하는 것이 중요합니다.
먼저 Navigation Controller를 정의합니다.탭뷰 탭의 선택 항목과 특정 뷰가 표시되는지 여부를 나타내는 부울 값을 추가했습니다.
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
}
탭뷰를 2개의 탭으로 설정하고 Navigation Controller를 바인드합니다.탭 보기로 선택:
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)
}
}
}
예를 들어, 이것은 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")
}
}
}
이 접근방식은 매우 신속합니다.UI-state 지향.
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()
}
}
저는 Swift에서 수 네비게이션의 를 하기 위해 를 UI, 【Swift】【Swift】【Swift】. »UINavigationController
。SceneDelegate
네비게이션 뷰
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)
}
}
후, 의 Swift my after after after my my after after after after after after after View, View를 .HasRootNavigationController
and extension (프로토콜 및 )
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
Malhal의 @Binding 솔루션 덕분에 저는 이 솔루션이 부족하다는 것을 알게 되었습니다..isDetailLink(false)
수식어
제 경우 이후의 모든 뷰에서 @Binding을 사용하고 싶지 않습니다.
이것이 Environment Object를 사용하고 있는 솔루션입니다.
1: 「」1: 「」을 합니다.AppState
Observable Object(관찰 객체)
import SwiftUI
import Combine
class AppState: ObservableObject {
@Published var moveToDashboard: Bool = false
}
스텝 2: 의 인스턴스를 생성하여 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: ★★의 3:★★ContentView.swift
는 ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★appState
입니다.이 은, 「」를 사용합니다..onReceive()
하여 contentView를 .isActive
Navigation Link(네비게이션 링크)는 false(거짓)입니다.
한 것은 '우리'를 사용하는 입니다..isDetailLink(false)
네비게이션 링크그렇지 않으면 작동하지 않습니다.
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()
}
}
이 솔루션은 malhal의 답변을 기반으로 Imthath와 Florin Odagiu의 제안을 사용하여 Paul Hudson의 Navigation View 비디오를 통해 모든 것을 통합해야 했습니다.
아이디어는 매우 간단하다.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")
}
}
이것은 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
}
}
다음은 Xcode 11 및 iOS 13.1에서 유효한 onAppear를 사용한 느린 애니메이션 및 약간 거친 역방향 팝 솔루션입니다.
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()
}
}
제 해결책은 이렇습니다.IT는 의존하지 않고 어디서나 작동합니다.
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)
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")
}
}
보기에서 호출하지 않고 Notification Center를 통해 호출할 수도 있습니다.
let backToRootItem = NavigationViewManager.BackToRootItem(tag: "nv1", animated: false, action: {})
NotificationCenter.default.post(name: .NavigationViewManagerBackToRoot, object: backToRootItem)
효과가 있는 다른 기술을 생각해 냈는데 아직도 이상한 느낌이 들어요.두 화면 모두 애니메이션으로 표시되지만 조금 더 깔끔합니다.A) 클로저를 후속 상세 화면으로 넘기거나 B) 상세 내역 B를 전달할 수 있습니다.presentationMode
상상 。B를 하고 detailA를 에 복귀할 까지 잠시 시켜야 합니다.둘 다 세부 정보 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() }
}
}
Swift가 어떻게 했는지를 생각하면 할수록UI가 동작하고, 사물의 구조가 구조화될수록, 애플이 제공하는 것과 동등한 것은 줄어들 것입니다.popToRootViewController
또는 네비게이션 스택을 직접 편집합니다.그것은 Swift의 길을 거스른다.UI는 하위 보기가 상위 상태에 도달하여 이를 조작할 수 있도록 하기 때문에 보기 구조를 구축합니다.이것이 바로 이러한 접근법이 하는 일입니다. 하지만 그들은 그것을 명백하고 노골적으로 합니다. DetailViewA
는, 자신의 상태에의 액세스를 제공하지 않고는, 어느쪽의 행선지 뷰도 작성할 수 없습니다.즉, 작성자는, 그 액세스의 제공의 의미를 심사숙고할 필요가 있습니다.
세부 사항
- 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
사용방법(짧은 샘플)
Navigation Link 만들기:
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)
}
}
}
저는 최근에 swiftui-navigation-stack이라는 오픈 소스 프로젝트를 만들었습니다.Swift를 위한 대체 내비게이션 스택입니다.UI. 자세한 것은, 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()
결과는 다음과 같습니다.
iOS 15 에서는, display() 를 사용해 subview 에 display 를 건네주는 간단한 솔루션이 있습니다.
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")
}
}
스위프트에서 해결책을 찾지 못했다.UI는 아직이지만 라이브러리는 깨끗합니다.UI.
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")
}
}
초등.루트 보기(돌아가려는 위치)에서 isActive 디자이너와 NavigationLink를 충분히 사용할 수 있습니다.마지막 뷰에서 isActive 파라미터를 제어하는 FALSE 변수로 전환합니다.
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"))
}
}
★★★로 Root View
하지 않고.isDetailLink(false)
를 지워야 요.NavigationLink
Root View
class NavigationLinkStore: ObservableObject {
static let shared = NavigationLinkStore()
@Published var showLink = false
}
struct NavigationLinkView: View {
@ObservedObject var store = NavigationLinkStore.shared
@State var isActive = false
var body: some View {
NavigationView {
VStack {
Text("Main")
Button("Go to View1") {
Task {
store.showLink = true
try await Task.sleep(seconds: 0.1)
isActive = true
}
}
if store.showLink {
NavigationLink(
isActive: $isActive,
destination: { NavigationLink1View() },
label: { EmptyView() }
)
}
}
}
}
}
struct NavigationLink1View: View {
var body: some View {
VStack {
Text("View1")
NavigationLink("Go to View 2", destination: NavigationLink2View())
}
}
}
struct NavigationLink2View: View {
@ObservedObject var store = NavigationLinkStore.shared
var body: some View {
VStack {
Text("View2")
Button("Go to root") {
store.showLink = false
}
}
}
}
여기에서는 복잡한 내비게이션에 대한 일반적인 접근방식을 설명하겠습니다.이 패턴은 루트에 팝업백할 필요가 있는 플로우가 1개뿐 아니라 여러 개 있는 경우에 도움이 됩니다.
먼저 환경 Observable Object를 설정하고 읽기 쉽도록 열거형을 사용하여 보기를 입력합니다.
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에서 EmptyView()의 NavigationLink 버튼을 사용합니다.태그와 선택 대신 NavigationLink의 isActive 파라미터를 사용하기 위해서입니다.메인 뷰의 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이든 Navigation Link(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
)
}
나는 나에게 잘 맞는 해결책을 찾았다.동작은 다음과 같습니다.
서서 ContentView.swift
삭제:
-
RootSelection
, 클래스 선언@EnvironmentObject
RootSelection
의 활성NavigationLink
루트 뷰에만 있습니다. - 를
.isDetailLink(false)
NavigationLink
최종 상세 뷰가 아닙니다. - 하여 ""를 .
NavigationView
. - 뷰에 여러 개의 루트 뷰가 정상적으로 합니다.
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())
입니다.ContentView()
에 의 xxxApp.swift
files.complete files files files files files files files files.
import SwiftUI
@main
struct DraftApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(RootSelection())
}
}
}
말할의 대답이 분명 맞는 답이야나는 에 대한 포장지를 만들었다.NavigationLink
수 되어 .isDetailLink(false)
필요한 모든 데이터를 캡처할 수 있습니다.
「 」, 「 」를 캡쳐 .isActive
"binding" 입니다.tag
이 경우 루트로 선언된 뷰로 팝업하고 싶을 때 리셋할 수 있습니다.
★★isRoot = true
되며, 에는 해당 뷰의 바인딩이 됩니다.dismiss
팝이 발생할 때 작업이 필요한 경우 파라미터는 옵션 닫기를 수행합니다.
는 Swift의 기본 .NavigationLink
단순한 부울 또는 태그 기반 네비게이션을 위한 이니셜라이저로 기존 사용법을 쉽게 편집할 수 있습니다.필요한 경우 다른 항목을 추가하는 것은 간단해야 합니다.
포장지는 다음과 같습니다.
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)
}
}
ViewRouter
필요한 건 뭐든지 될 수 있어요사용하였습니다.ObservableObject
으로는 '어느 정도'를 Published
「 」 、 「 」 、 「 」:
class ViewRouter: ObservableObject {
var root: Binding<Bool>?
typealias Dismiss = () -> Void
var dismissals : [Dismiss] = []
func popToRoot() {
dismissals.forEach { dismiss in
dismiss()
}
dismissals = []
root?.wrappedValue = false
}
}
처음에는 여기에 게시된 Chuck H의 솔루션을 사용하고 있었습니다.
하지만 이 솔루션이 제 경우 효과가 없을 때 저는 문제에 직면했습니다.는 "2"를 할 수 있는 되었습니다.pop to root
이 경우 이 솔루션은 하나의 공통 상태를 가지고 있기 때문에 작동하지 않았습니다.@Environment(\.rootPresentationMode) private var rootPresentationMode
는 the든 the 를 만들었다.RouteManager
열거형 " " "을 하여"를 지정합니다.Route
, 가 「이러한 플로우」를할 수 있는 를 나타냅니다.pop to root
Route Manager:
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
의 of의 RouteManager
사용자가 흐름으로 이동한 후 뒤로 버튼을 눌러 되돌아가는 경우. 이전에 한 흐름의 않은치 않은).「 」 、 「 」 、 「 」( 「 」 ) 。
.onAppear {
routeManager.unselect(.signUp)
routeManager.unselect(.restorePassword)
}
완전한 데모 프로젝트는 이쪽에서 보실 수 있습니다.
하다 보면 NavigationView
★★★★★★★★★★★★★★★★★」NavigationLink
단, 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")
}
}
저는 "그냥 작동"하는 솔루션을 만들었고, 매우 만족합니다.마법 솔루션을 사용하려면 몇 가지 단계만 수행하면 됩니다.
먼저 이 스레드에서 사용되는 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
Navigation ('네비게이션뷰')에 대해서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")
}
}
}
}
@malhal의 답변이 도움이 되었지만, 제 상황에서는 버튼을 하나씩 눌러 탐색할 때 기능이 필요했습니다.같은 처지라면 이 코드를 사용해 보세요!
// 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")
}
}
같은 문제는 없지만 루트 뷰를 네비게이션스택을 지원하지 않는 뷰에서 지원하는 뷰로 변경하는 코드가 있습니다.비결은 스위프트에서는 하지 않는다는 것이다.UI - 이 작업은SceneDelegate
를 합니다.UIHostingController
새것으로.
간략한 .SceneDelegate
:
func changeRootToOnBoarding() {
guard let window = window else {
return
}
let onBoarding = OnBoarding(coordinator: notificationCoordinator)
.environmentObject(self)
window.rootViewController = UIHostingController(rootView: onBoarding)
}
func changeRootToTimerList() {
guard let window = window else {
return
}
let listView = TimerList()
.environmentObject(self)
window.rootViewController = UIHostingController(rootView: listView)
}
★★★★★★★★★★★★★★★★★.SceneDelegate
할 수 있는
/// Our "parent" SceneDelegate that can change the root view.
@EnvironmentObject private var sceneDelegate: SceneDelegate
그리고 나서 대리인의 공개 행사에 호출합니다.그, 그 행동을 유지했을 거라고 생각합니다.View
냈습니다.UIHostingController
그것을 위해 교환했다.window.rootViewController
당신에게 도움이 될 수도 있어요
언급URL : https://stackoverflow.com/questions/57334455/how-can-i-pop-to-the-root-view-using-swiftui
'bestsource' 카테고리의 다른 글
다른 페이지에서 로그인이 필요한 페이지에 액세스하기 위한 CURL (0) | 2023.04.19 |
---|---|
ListView에서 부모로 스크롤 이벤트 버블링 (0) | 2023.04.19 |
VBA를 사용하여 파일이 있는지 확인합니다. (0) | 2023.04.14 |
bash completion을 에일리어스로 작업하려면 어떻게 해야 하나요? (0) | 2023.04.14 |
여러 줄 명령어 내의 Bash 스크립트에 대한 코멘트 (0) | 2023.04.14 |