SwiftUI - Navigating back to home without nesting? - swiftui

In the code below, if I use the links to go back and forth between views A and B, I will end up with nested views as shown in the image. The only way I've found to avoid nesting is to never link to a view where a NavigationView is declared - as it is in ViewA below. My question...is there any way to go back to ViewA without the views nesting?
struct ViewA: View {
var body: some View {
NavigationView{
NavigationLink(destination: ViewB()) {
Text("ViewB")
}
}
.navigationBarTitle("ViewA")
}
}
struct ViewB: View {
var body: some View {
NavigationLink(destination: ViewA()) {
Text("ViewA")
}
.navigationBarTitle("ViewB")
}
}

You should not create NavigationLink(destination: ViewA()) because it is not back it creates a new ViewA. Once you navigate to ViewB, the back button will be create for you automatically.
struct ViewA: View {
var body: some View {
NavigationView{
NavigationLink(destination: ViewB()) {
Text("ViewB")
}
}
.navigationBarTitle("ViewA")
}
}
struct ViewB: View {
var body: some View {
Text("ViewB Pure Content")
.navigationBarTitle("ViewB")
}
}

You are nesting views because every time you click ViewA/ViewB it creates a new view object. You can add
#Environment(\.presentationMode) var presentationMode
and call
presentationMode.wrappedValue.dismiss()
when the view button gets pressed you dismiss it.

Related

SwiftUI - TabView/NavigationLink navigation breaks when using a custom binding

I'm having trouble with what I think may be a bug, but most likely me doing something wrong.
I have a slightly complex navigation state variable in my model that I'm using for tracking/setting state between tab and sidebar presentations when multitasking on iPad. That all works fine except in tab mode, once I use a navigation link once I can't seem to use one again, whether the binding is on my tab view or navigation links in a list.
Would really appreciate any thoughts on this,
Cheers!
Example
NavigationItem.swift
enum SubNavigationItem: Hashable {
case overview, user, hobby
}
enum NavigationItem: Hashable {
case home(SubNavigationItem)
case settings
}
Model.swift
final class Model: ObservableObject {
#Published var selectedTab: NavigationItem = .home(.overview)
}
SwiftUIApp.swift
#main
struct SwiftUIApp: App {
#StateObject var model = Model()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(model)
}
}
}
ContentView.swift
struct ContentView: View {
var body: some View {
AppTabNavigation()
}
}
AppTabNavigation.swift
struct AppTabNavigation: View {
#EnvironmentObject private var model: Model
var body: some View {
TabView(selection: $model.selectedTab) {
NavigationView {
HomeView()
}
.tabItem {
Label("Home", systemImage: "house")
}
.tag(NavigationItem.home(.overview))
NavigationView {
Text("Settings View")
}
.tabItem {
Label("Settings", systemImage: "gear")
}
.tag(NavigationItem.settings)
}
}
}
HomeView.swift
I created a binding here because selection required an optional <NavigationItem?> not
struct HomeView: View {
#EnvironmentObject private var model: Model
var body: some View {
let binding = Binding<NavigationItem?>(
get: {
model.selectedTab
},
set: {
guard let item = $0 else { return }
model.selectedTab = item
}
)
List {
NavigationLink(
destination: Text("Users"),
tag: .home(.user),
selection: binding
) {
Text("Users")
}
NavigationLink(
destination: Text("Hobbies"),
tag: .home(.hobby),
selection: binding
) {
Text("Hobbies")
}
}
.navigationTitle("Home")
}
}
Second Attempt
I tried making the selectedTab property optional as #Lorem Ipsum suggested. Which means I can remove the binding there. But then the TabView doesn't work with the property. So I create a binding for that and have the same issue but with the tab bar!
Make the selected tab optional
#Published var selectedTab: NavigationItem? = .home(.overview)
And get rid of that makeshift binding variable. Just use the variable
$model.selectedTab
If the variable can never be nil then something is always selected IAW with that makeshift variable it will just keep the last value.

.onAppear is calling when I navigated back to a view by clicking back button

I have two views written in swiftUI , say for example ViewA and ViewB.
onAppear() of ViewA has an apiCall which calls when initially the view is loaded.
I navigate to ViewB from ViewA using navigation link and on clicking back button in ViewB the onAppear() of ViewA is called.
• Is there any way to stop calling onAppear() while navigated back from a view
• I am looking swiftUI for something like 'ViewDidLoad' in UIKit
given a sample of my code
struct ContentView: View {
var body: some View {
NavigationView{
List(viewModel.list){ item in
NavigationLink(
destination: Text("Destination"),
label: {
Text(item.name)
})
}
.onAppear{
viewModel.getListApiCall()
}
}
}
}
Overview
SwiftUI is quite different from the way UIKit works.
It would be best to watch the tutorials (links below) to understand how SwiftUI and Combine works.
SwiftUI is a declarative framework so the way we approach is quite different. It would be best not to look for a direct comparison to UIKit for equivalent functions.
Model:
Let the model do all the work of fetching and maintaining the data
Ensure that your model conforms to ObservableObject
When ever any #Published property changes, it would imply that the model has changed
View:
Just display the contents of the model
By using #ObservedObject / #EnvironmentObject SwiftUI would observe the model and ensure that the view states in sync with any changes made to the model
Notice that though the model fetches the data after 2 seconds, the view reacts to it and displays the updated data.
Model Code:
class Model: ObservableObject {
#Published var list = [Item]()
init() {
fetchItems()
}
private func fetchItems() {
//To simulate some Async API call
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
self?.list = (1...10).map { Item(name: "name \($0)") }
}
}
}
struct Item: Identifiable {
var name: String
var id : String {
name
}
}
View Code:
import SwiftUI
struct ContentView: View {
#ObservedObject var model: Model
var body: some View {
NavigationView{
List(model.list){ item in
NavigationLink(destination: Text("Destination")) {
Text(item.name)
}
}
}
}
}
Reference:
SwiftUI
https://developer.apple.com/videos/play/wwdc2020/10119
https://developer.apple.com/videos/play/wwdc2020/10037
https://developer.apple.com/videos/play/wwdc2020/10040
Combine
https://developer.apple.com/videos/play/wwdc2019/722
https://developer.apple.com/videos/play/wwdc2019/721
https://developer.apple.com/videos/play/wwdc2019/226
You could add a variable to check if the getListApiCall() has been invoked.
struct ContentView: View {
#State var initHasRun = false
var body: some View {
NavigationView{
List(viewModel.list){ item in
NavigationLink(
destination: Text("Destination"),
label: {
Text(item.name)
})
}
.onAppear{
if !initHasRun {
viewModel.getListApiCall()
initHasRun=true
}
}
}
}
}

PresentationMode.dismiss weird behaviour when using multiple NavigationLinks inside ForEach

My app has 4 views (let's call them View_A[root] -> View_B -> View_C -> View_D). The navigation between them was made using NavigationView/NavigationLink.
When I call self.presentationMode.wrappedValue.dismiss() from the last view(View_D) I expect it to dismiss the current view (D) only, but for some reason it dismissed ALL the views and stops at view A (root view).
That's weird.
I spent a couple of hours trying to figure out what's going on there and I found that
- if I remove "ForEach" from "View_A" it works correctly and only the last view is dismissed. Even though ForEach gets just 1 static object in this example.
The second weird thing is that
- if I don't change "self.thisSession.stats" to false it also works correctly dismissing only the last view.
This is super weird as View_A (as far as I understand) is not dependent on thisSession environment variable.
Any ideas on how to prevent View_C and View_B from being dismissed in this case? I wanna end up at View_C after clicking the link, not at View_A.
Any help is appreciated, it took me a while to find out where it comes from but I'm not smart enough to proceed any further ;)
import SwiftUI
struct A_View: View {
#EnvironmentObject var thisSession: CurrentSession
var body: some View {
NavigationView {
VStack {
Text("View A")
ForEach([TestObject()], id: \.id) { _ in
NavigationLink(destination: View_B() ) {
Text("Move to View B")
}
}
}
}
}
}
struct View_B: View {
var body: some View {
NavigationView {
NavigationLink(destination: View_C()
) {
Text("GO TO VIEW C")
}
}
}
}
struct View_C: View {
var body: some View {
ZStack {
NavigationView {
NavigationLink(destination: View_D()) {
Text("GO TO VIEW D")
}
}
}
}
}
struct View_D: View {
#EnvironmentObject var thisSession: CurrentSession
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
ZStack {
VStack {
Button(action: {
self.thisSession.stats = false
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Return!")
}
}
}
}
}
class CurrentSession: ObservableObject {
#Published var stats: Bool = false
#Published var user: String = "user"
}
struct TestObject: Identifiable, Codable {
let id = UUID()
}
Your issue is with:
NavigationView
There is only supposed to be one NavigationView in an entire view stack. Try removing the NavigationView from views B and C

How to pop multiple views off a navigation stack?

Looking for some guidance on a simple way to pop multiple views off a navigation stack in SwiftUI.
I have 4 views chained together using NavigationLink. At the last view I would like to jump back to the initial ContentView, popping all the other views off the stack. I don't want to use the "Back" button on the NavigationBar of each view to achieve this.
Thanks in advance.
Bob.
'''
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: BView()) {
Text("This is View A, now go to View B.")
}
}
}
}
}
struct BView: View {
var body: some View {
NavigationLink(destination: CView()) {
Text("This is View B, now go to View C.")
}
}
}
struct CView: View {
var body: some View {
NavigationLink(destination: DView()) {
Text("This is View C, now go to View D.")
}
}
}
struct DView: View {
var body: some View {
// The following line adds ContentView onto the existing navigation stack. Instead, I want to pop the previous views off the stack, leaving me back at ContentView.
NavigationLink(destination: ContentView()) {
Text("This is View D, now jump back to View A.")
}
}
}
'''
It's not really "popping" views off of the stack, but your SceneDelegate can set the rootViewController to any View you want (see line 28 of default SceneDelegate.swift). In your case you want it to be ContentView again.
For example in your SceneDelegate add something like:
func toContentView() {
let contentView = ContentView()
window?.rootViewController = UIHostingController(rootView: contentView)
}
Then in DView, change the NavigationLink to a Button that just does:
(UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.toContentView()
If you have multiple scenes, you'll need a bit more.
Making Cenk Bilgen's answer more generic.
struct RootView {
static func change(to view: AnyView) {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let sceneDelegate = windowScene.delegate as? SceneDelegate else {
return
}
let contentView = view
sceneDelegate.window?.rootViewController = UIHostingController(rootView: contentView)
}
}
Usage:
RootView.change(to: AnyView(DashboardView()))

SwiftUI Navigation Multiple back button

When I push more than one view, multiple back buttons are visible in the navigation bar.
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination:SecView()) {
Text("Primo")
}
}
}
}
struct SecView: View {
var body: some View {
NavigationView {
NavigationLink(destination:TerView()) {
Text("Secondo")
}
}
}
}
struct TerView: View {
var body: some View {
Text("Hello World!")
}
}
I would like to have only one back button per view.
Here is a screenshot of the problem.
There should only be a single NavigationView at the root of your navigation stack.
Remove the NavigationView block from SecView and you will then have a single navigation bar owned by ContentView.
as Gene said, there should only be a single NavigationView at the root of you navigation stack. This means that the next time you need to navigate to a page in a different View, you will add a NavigationLink but not wrap it in a NavigationView. So in the code that you initially posted, you need to remove the NavigationView from your SecView View but still keep the NavigationLink. See the code below:
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination:SecView()) {
Text("Primo")
}
}
}
}
struct SecView: View {
var body: some View {
NavigationLink(destination:TerView()) {
Text("Secondo")
}
}
}
struct TerView: View {
var body: some View {
Text("Hello World!")
}
}