How to preserve pushed view when moving between tabs? - ios

I have noticed that TabView has another behavior than UITabBarController, the view stack is cleared each time the user moves from one tab to another.
I have created a small sample to illustrate my case:
struct ContentView: View {
#State private var selection = 0
var body: some View {
TabView(selection: $selection) {
NavView(id: "A").tabItem { Text("Alpha") }.tag(0)
NavView(id: "B").tabItem { Text("Beta") }.tag(1)
}
}
}
struct NavView: View {
var id: String
var body: some View {
NavigationView {
List {
NavigationLink(destination: Text("Detail \(id)")) { Text(id) }
}
}
}
}
The detail view is gone since the NavView is recreated after a visit to the other tab.
Question: Is there a way to recreate the old behavior, that a presented detail view is still present after a visit on another tab?

Related

Multiple back buttons created when navigates to root screen in SwiftUI

say I create 3 screens in SwiftUI which contains a NavigationLink to the next screen. like, first screen navigates to 2nd screen. 2nd screen navigates to third. and the third screen navigates to the first screen. In this case even if I use NavigationView only once(in the first screen). I encountered that there's a back button forming when I navigate to 1st screen from the third screen. And it keeps adding up when I start to navigate from then on. I have tried to use .navigationBarBackButtonHidden(true). It hides it but the space taken by back button was still there.
My code is similar to this:
struct FirstScreen: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: SecondScreen()) {
Text("Go to Second Screen")
}
}
}
}
}
struct SecondScreen: View {
var body: some View {
VStack {
NavigationLink(destination: ThirdScreen()) {
Text("Go to Third Screen")
}
}
}
}
struct ThirdScreen: View {
var body: some View {
VStack {
NavigationLink(destination: FirstScreen()) {
Text("Go to First Screen")
}
}
}
}
this is the image
You're pushing the FirstScreen onto your navigation stack, but FirstScreen contains its own NavigationView. If you really want to keep pushing them on the stack, then move the NavigationView outside of FirstScreen.
struct ContentView: View {
var body: some View {
NavigationStack { // Use NavigationStack for iOS 16
FirstScreen()
}
}
}
struct FirstScreen: View {
var body: some View {
VStack {
NavigationLink(destination: SecondScreen()) {
Text("Go to Second Screen")
}
}
}
}
If you actually want to pop all the views off the stack and go back to FirstScreen you should use
init(path: Binding<NavigationPath>, etc)
Here's a simple example passing the path down the stack and resetting it to pop back to the root…
enum Screen {
case two, three
}
struct ContentView: View {
#State var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
VStack {
// value gets appended to path
NavigationLink("Go to Second Screen", value: Screen.two)
}
// Decides which screen to show for values in path
.navigationDestination(for: Screen.self) { index in
switch index {
case .two:
SecondScreen(path: $path)
case .three:
ThirdScreen(path: $path)
}
}
}
}
}
struct SecondScreen: View {
#Binding var path: NavigationPath
var body: some View {
VStack {
NavigationLink("Go to Third Screen", value: Screen.three)
}
}
}
struct ThirdScreen: View {
#Binding var path: NavigationPath
var body: some View {
VStack {
Button("Go to First Screen") {
// reset the path to pop to root
path = NavigationPath()
}
}
}
}

Unwind NavigationView to root when switching tabs in SwiftUI

I have an app with a few tabs, and on one of those there is a NavigationLink which nests a couple of times.
I want to be able to switch tabs, and when going back to the other tab to have unwound all links to the root view.
I have seen these: https://stackoverflow.com/a/67014642/1086990 and https://azamsharp.medium.com/unwinding-segues-in-swiftui-abdf241be269 but they seem to be focusing on unwinding when active on the view, not switching from it.
struct MyTabView: View {
var body: some View {
TabView {
TabOne().tabItem { Image(systemName: "1.square") }
TabTwo().tabItem { Image(systemName: "2.square") }
}
}
}
struct TabOne: View {
var body: some View {
Text("1")
}
}
struct TabTwo: View {
var body: some View {
NavigationView {
NavigationLink("Go to sub view") {
TabTwoSub()
}
}
}
}
struct TabTwoSub: View {
var body: some View {
Text("Tapping \(Image(systemName: "1.square")) doesnt unwind this view back to the root of the NavigationView")
.multilineTextAlignment(.center)
}
}
Maybe I've missed something fairly basic but nothing seems to come up from searches on unwinding views when switching tabs.
I tried using the NavigationLink(isActive: , destination: , label: ) from the other SO answer but couldn't get it working in the root MyTabView.
I thought about using UserDefaults to set a isActive bool state and if not try and unwind the navigation, but that didn't seem very swifty to do.
What is happening
You'll need to keep track of the tab selection in the parent view and then pass that into the child views so that they can watch for changes. Upon seeing a change in the selection, the child view can then reset a #State variable that change the isActive property of the NavigationLink.
class NavigationManager : ObservableObject {
#Published var activeTab = 0
}
struct MyTabView: View {
#StateObject private var navigationManager = NavigationManager()
var body: some View {
TabView(selection: $navigationManager.activeTab) {
TabOne().tabItem { Image(systemName: "1.square") }.tag(0)
TabTwo().tabItem { Image(systemName: "2.square") }.tag(1)
}.environmentObject(navigationManager)
}
}
struct TabOne: View {
var body: some View {
Text("1")
}
}
struct TabTwo: View {
#EnvironmentObject private var navigationManager : NavigationManager
#State private var linkActive = false
var body: some View {
NavigationView {
NavigationLink("Go to sub view", isActive: $linkActive) {
TabTwoSub()
}
}.onChange(of: navigationManager.activeTab) { newValue in
linkActive = false
}
}
}
struct TabTwoSub: View {
var body: some View {
Text("Tapping \(Image(systemName: "1.square")) doesnt unwind this view back to the root of the NavigationView")
.multilineTextAlignment(.center)
}
}
Note: this will result in a "Unbalanced calls to begin/end appearance transitions" message in the console -- in my experience, this is not an error and not something we have to worry about

How to dismiss a presenting view to the root view of tab view in SwiftUI?

I'm using TabView on my home page. Let's just say I have 4 tabs.
On second tab, i can go to another view using NavigationLink and I go to another 2 views using NavigationLink. Then on the latest view, there is a button to present a view and i use .fullScreenCover (since I want to present it full screen).
In the presenting view, I add an X mark on the left side of the navigationBarItems to dismiss. I use #Environment(\.presentationMode) var presentationMode and presentationMode.wrappedValue.dismiss() to dismiss. But it only dismiss the presenting view to the previous view, while actually I want to dismiss it to the root of my view which is the 2nd tab of my TabView.
Is there a way to do this? Because I have looked up to some articles and nothing relevant especially in TabView context.
I also have a question tho:
Is it a right approach to use .fullScreenCover? Or is there another possible solution for example presenting a modal with full screen style (if there's any cause i'm not sure either).
Any suggestions will be very appreciated, thankyou in advance.
The presentationMode is one-level effect value, ie changing it you close one currently presented screen.
Thus to close many presented screens you have to implement this programmatically, like in demo below.
The possible approach is to use custom EnvironmentKey to pass it down view hierarchy w/o tight coupling of every level view (like with binding) and inject/call only at that level where needed.
Demo tested with Xcode 12.4 / iOS 14.4
struct ContentView: View {
var body: some View {
TabView {
Text("Tab1")
.tabItem { Image(systemName: "1.square") }
Tab2RootView()
.tabItem { Image(systemName: "2.square") }
}
}
}
struct Tab2RootView: View {
#State var toRoot = false
var body: some View {
NavigationView {
Tab2NoteView(level: 0)
.id(toRoot) // << reset to root !!
}
.environment(\.rewind, $toRoot) // << inject here !!
}
}
struct Tab2NoteView: View {
#Environment(\.rewind) var rewind
let level: Int
#State private var showFullScreen = false
var body: some View {
VStack {
Text(level == 0 ? "ROOT" : "Level \(level)")
NavigationLink("Go Next", destination: Tab2NoteView(level: level + 1))
Divider()
Button("Full Screen") { showFullScreen.toggle() }
.fullScreenCover(isPresented: $showFullScreen,
onDismiss: { rewind.wrappedValue.toggle() }) {
Tab2FullScreenView()
}
}
}
}
struct RewindKey: EnvironmentKey {
static let defaultValue: Binding<Bool> = .constant(false)
}
extension EnvironmentValues {
var rewind: Binding<Bool> {
get { self[RewindKey.self] }
set { self[RewindKey.self] = newValue }
}
}
struct Tab2FullScreenView: View {
#Environment(\.presentationMode) var mode
var body: some View {
Button("Close") { mode.wrappedValue.dismiss() }
}
}
You have 2 options:
With .fullScreenCover you will have a binding that results in it being presented you can pass this binding through to the content and when the user taps on x set to to false
You can use the #Environment(\.presentationMode) var presentationMode then call presentationMode.wrappedValue.dismiss() in your button body.
Edit:
If you want to unwind all the way you should make the TabView be binding based. I like to use SceneStorage for this take a look at this post then you can access this SceneStorage value anywhere in your app to respond to it but also to update and change the navigation (this also has the benefit of providing you proper state restoration!)
If you make your TabView in this way:
struct ContentView: View {
#SceneStorage("selectedTab") var selectedTab: Tab = .car
var body: some View {
TabView(selection: $selectedTab) {
CarTrips()
.tabItem {
Image(systemName: "car")
Text("Car Trips")
}.tag(Tab.car)
TramTrips()
.tabItem {
Image(systemName: "tram.fill")
Text("Tram Trips")
}.tag(Tab.tram)
AirplaneTrips()
.tabItem {
Image(systemName: "airplane")
Text("Airplane Trips")
}.tag(Tab.airplaine)
}
}
}
enum Tab: String {
case car
case tram
case airplaine
}
Then deep within your app in the place you want to change the navigation you can create a button view.
struct ViewCarButton: View {
#SceneStorage("selectedTab") var selectedTab: Tab = .car
var body: some View {
Button("A Button") {
selectedTab = .car
}
}
}
This will forced the selected tab to be the car tab.
if instead of this you do not want to change tab but rather change what the navigation view is navigated to you can use the same concept for that, NavigationLink that's a binding if this binding is created using a #SceneStorage then in your ViewCarButton you can make changes to it that will change the navigation state.

SwiftUI - Nested NavigationLink

I have a layout that looks like this:
Layout Drawing
There is a main view which is the Feed that would be my NavigationView and then I have views inside: PostList -> Post -> PostFooter and in the PostFooter A Button that would be my NavigationLink
struct Feed: View {
var body: some View {
NavigationView {
PostList()
}
}
}
struct PostList: View {
var body: some View {
List {
ForEach(....) {
Post()
}
}
}
}
struct Post: View {
var body: some View {
PostHeader()
Image()
PostFooter()
}
}
struct PostFooter: View {
var body: some View {
NavigationLink(destination: Comment()) {
Text("comments")
}
}
}
But When when I tap on the comments, it goes to the Comment View then go back to Feed() then back to Comment() then back to Feed() and have weird behaviour.
Is there a better way to handle such a situation ?
Update
The Navigation is now working but the all Post component is Tapeable instead of just the Text in the PostFooter.
Is there any way to disable tap gesture on the cell and add multiple NavigationLink in a cell that go to different pages ?
How about programmatically active the NavigationLink, for example:
struct PostFooter: View {
#State var commentActive: Bool = false
var body: some View {
VStack{
Button("Comments") {
commentActive = true
}
NavigationLink(destination: Comment(), isActive: $commentActive) {
EmptyView()
}
}
}
}
Another benefit of above is, your NavigationLink destination View can accept #ObservedObject or #Binding for comments editing.

Navigate to same view in SwiftUI

So I'm trying to navigate from one view to another in SwiftUI and have stumbled upon a problem.
My views looks like this and trying to navigation from one DetailView to another DetailView but as soon I push the navigation link I'm forced back to the first DetailView.
Any ideas on how to achieve navigation to the same view in SwiftUI?
ListView.swift
struct ListView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView(data: data)) {
Text("Navigate")
}
}
}
}
}
DetailView.swift
struct DetailView: View {
var body: some View {
VStack {
NavigationLink(destination: DetailView(data: otherData)) {
Text("Navigate")
}
}
}
}
EDIT:
Remove the NavigationView in DetailView as pointed out below but still facing the same issue.
Remove NavigationView from DetailView, there should be only one in stack, so when you navigate all new links are opened in original NavigationView:
struct DetailView: View {
var body: some View {
VStack {
NavigationLink(destination: DetailView(data: otherData)) {
Text("Navigate")
}
}
}
}

Resources