Programatical navigation on NavigationView deeper than two views - swift

Is there a way I can do programatical navigation with more than two views using NavigationView?
Like this: View 1 -> View 2 -> View 3
This is the sample code for what I'm trying to do:
class Coordinator: ObservableObject {
#Published var selectedTag: String?
}
struct ContentView: View {
#EnvironmentObject var coordinator: Coordinator
let things = ["first", "second", "third"]
var body: some View {
NavigationView {
List(things, id: \.self) { thing in
NavigationLink(destination: SecondView(thing: thing),
tag: thing,
selection: self.$coordinator.selectedTag) {
Text(thing)
}
}
}
}
}
struct SecondView: View {
#EnvironmentObject var coordinator: Coordinator
let thing: String
let things = ["fourth", "fifth", "sixth"]
var body: some View {
List(things, id: \.self) { thing2 in
NavigationLink(destination: ThirdView(thing: self.thing, thing2: thing2),
tag: thing2,
selection: self.$coordinator.selectedTag) {
Text(thing2)
}
}
}
}
struct ThirdView: View {
let thing: String
let thing2: String
var body: some View {
Text("\(thing) \(thing2)")
}
}
I hoped that I could select a specific tag and deep navigate to the ThirdView but even the simple navigation won't work. If I select a link on the SecondView it will navigate forward and then back, instead of just navigating forward as expected.
I also tried using 2 variables, one to represent the tag of each screen but it also doesn't work.
Is there a way to make this work? Am I doing something wrong?

Currently, your code is acting like there's another flow of navigation from SecondView to ThirdView, which I assume you're not intending to. If that's the intended case, you should also wrap SecondView's body into a NavigationView, too.
Caveat: You'll have 2 navigation bars.
Without tag and selection (in SecondView or also in ContentView), your code works as intended. Is there a specific reason for using tag and selection here? I couldn't find a proper solution that both uses them and have only one NavigationView.

Related

Is it possible to create a toolbar item without using navigationView/navigationStack in SwiftUI?

See the code below. Using that code I can see the toolbar item without any problem. But if i remove or replace navigationView with for example a VSack the toolbar item disappears.
Is it possible to add a toolbar item without navigationView/navigationStack? I extra interested in the possibility since it may solve my problem with using multiple navigationViews in multiple views connected to each other.
The working code:
import SwiftUI
struct Restaurant: Identifiable {
let id = UUID()
let name: String
}
// A view that shows the data for one Restaurant.
struct RestaurantRow: View {
var restaurant: Restaurant
var body: some View {
Text("Come and eat at \(restaurant.name)")
}
}
// Create three restaurants, then show them in a list.
struct SwiftUIView: View {
let restaurants = [
Restaurant(name: "Joe's Original"),
Restaurant(name: "The Real Joe's Original"),
Restaurant(name: "Original Joe's")
]
var body: some View {
NavigationStack {
List(restaurants) { restaurant in
RestaurantRow(restaurant: restaurant)
}
.toolbar {
ToolbarItem(placement: .automatic) {
Text("Great Toolbar")
}
}
}
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView()
}
}
The code that doesn't show the toolbar item:
var body: some View {
VStack {
List(restaurants) { restaurant in
RestaurantRow(restaurant: restaurant)
}
.toolbar {
ToolbarItem(placement: .automatic) {
Text("Great Toolbar")
}
}
}
}
}
Removed navigationView, replaced it with something else like a VStack.
On iOS (and iPadOS), the toolbar modifier requires an enclosing NavigationView or NavigationStack. However, as I explained when answering your other question, you only need one enclosing NavigationView. The toolbar modifier will search up the view hierarchy to find it.
Note though that if you present a view in a sheet, you will need a NavigationView inside that sheet. The sheet has its own view hierarchy independent of the view that presents the sheet.

SwiftUI #ObservedObject viewmodel in detail-view of List never released [duplicate]

This question already has answers here:
ObservedObject view-model is still in memory after the view is dismissed
(2 answers)
Closed 2 years ago.
I have a List with several items, that open a DetailView which in turn holds a viewmodel. The viewmodel is supposed to have a service class that gets initialized when the detail view appears and should be deinitialized when navigating back.
However, the first problem is that in my example below, all 3 ViewModel instances are created at the same time (when ContentView is displayed) and never get released from memory (deinit is never called).
struct ContentView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: DetailView()) {
Text("Link")
}
NavigationLink(destination: DetailView()) {
Text("Link")
}
NavigationLink(destination: DetailView()) {
Text("Link")
}
}
}
}
}
struct DetailView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
Text("Hello \(viewModel.name)")
}
}
class ViewModel: ObservableObject {
#Published var name = "John"
private let heavyClient = someHeavyService()
init() { print("INIT VM") }
deinit { print("DEINIT VM") }
}
This is probably just how SwiftUI works, but I have a hard time thinking of a way to handle class objects that are part of a detail view's state, but are not supposed to instantiate until the detail view actually appears. An example would be video conferencing app, with rooms, where the room client that establishes connections etc. should only get initialized when actually entering the room and deinitialize when leaving the room.
I'd appreciate any suggestions on how to mange this. should I initialize the heavyClient at onAppear or something similar?
The problem is that DetailView() is getting initialized as part of the navigation link. One possible solution could be the LazyView from this post.
Implemented like so:
struct LazyView<Content: View>: View {
let build: () -> Content
init(_ build: #autoclosure #escaping () -> Content) {
self.build = build
}
var body: Content {
build()
}
}
And then wrap the DetailView() in the LazyView():
struct ContentView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: LazyView(DetailView())) {
Text("Link")
}
NavigationLink(destination: LazyView(DetailView())) {
Text("Link")
}
NavigationLink(destination: LazyView(DetailView())) {
Text("Link")
}
}
}
}
}
The only issue with this workaround is that there seems to always be one instance of ViewModel sitting around, though it's a large improvement.

What is the lifecycle of #State variables in SwiftUI?

If I create a new #State variable, when does it get destroyed? Does it live for the lifetime of the parent UIHostingController?
As far as I can find, it is not documented. This is relevant because I don't understand how to clean up after myself if I create an ObservableObject as State somewhere in the view hierarchy.
import SwiftUI
struct Example: View {
#State private var foo = Foo()
var body: some View {
Text("My Great View")
}
}
class Foo: ObservableObject {
deinit {
// When will this happen?
print("Goodbye!")
}
}
Assuming:
struct Example: View {
#State private var foo = Foo()
var body: some View {
Text("My Great View")
}
}
class Foo: ObservableObject {
init() {
print(#function)
}
deinit {
print(#function)
}
}
The issue is that a View type is a struct, and it's body is not a collection of functions that are executed in real-time but actually initialized at the same time when View's body is rendered.
Problem Scenario:
struct ContentView: View {
#State var isPresented = false
var body: some View {
NavigationView {
NavigationLink(destination: Example()) {
Text("Test")
}
}
}
}
If you notice, Example.init is called before the navigation even occurs, and on pop Example.deinit isn't called at all. The reason for this is that when ContentView is initialized, it has to initialize everything in it's body as well. So Example.init will be called.
When we navigate to Example, it was already initialized so Example.init is not called again. When we pop out of Example, we just go back to ContentView but since Example might be needed again, and since it is not created in real-time, it is not destroyed.
Example.deinit will be called only when ContentView has to be removed entirely.
I wasn't sure on this but found another article talking about a similar issue here:
SwiftUI and How NOT to Initialize Bindable Objects
To prove this, lets ensure the ContentView is being completely removed.
The following example makes use of an action sheet to present and remove it from the view hierarchy.
Working Scenario:
struct ContentView: View {
#State var isPresented = false
var body: some View {
Button(action: { self.isPresented.toggle() }) {
Text("Test")
}
.sheet(isPresented: $isPresented) {
Example()
.onTapGesture {
self.isPresented.toggle()
}
}
}
}
PS: This applies to classes even if not declared as #State, and does not really have anything to do with ObservableObject.
In iOS 14, the proper way to do this is to use #StateObject. There is no safe way to store a reference type in #State.

SwiftUI - Presenting a View on top of the current View from AppDelegate

I am working on a project that at some point it receives a notification. When that happens, I need to show a View. I am not able to catch notification from any View so I am looking for a way to change to control it from outside of View structs. After the View's purpose is done, I need to dismiss it where the app left off. Think like the native behaviour when there is an active call.
I thought I could use sheet however I could not find any way to trigger it for every View that could be active when the notifications come. Or maybe trying to extend native View class would work but again, no luck finding a tutorial.
Any help will be appreciated.
Just update your model based on notification. There is not necessary to define .sheet (modal view) everywhere in your view hierarchy. Doing it in root view should be enough.
To demonstrate that (copy - paste - run) I create small project where I mimic notification with SwiftUI Toggle.
import SwiftUI
class Model: ObservableObject {
#Published var show = false
}
struct SubView: View {
#EnvironmentObject var model: Model
var tag: Int
var body: some View {
VStack {
NavigationLink(destination: SubView(tag: tag + 1).environmentObject(model)) {
Text("subview \(tag)")
}
if tag == 2 {
Toggle(isOn: $model.show) {
Text("toggle")
}.padding()
}
}.navigationBarTitle("subview \(tag)")
}
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
NavigationView {
SubView(tag: 0).environmentObject(model)
}.sheet(isPresented: $model.show) {
Text("sheet")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
with the result

Removing large amounts of whitespace in a SwiftUI subview

Demonstration of whitespace problem
When I nest a NavigationView within a NavigationView, an enormous amount of whitespace separates the back button and the new navigation bar title. Is there something I'm doing wrong in terms of setting up my SwiftUI views?
import SwiftUI
struct Dashboard: View {
#EnvironmentObject var user: User
let courses = Course.exampleCourses()
var body: some View {
NavigationView {
List(courses) { course in
NavigationLink(destination: CourseView(course: course)) {
Text(course.name)
}
}.navigationBarTitle("Welcome, \(user.first)!")
}
}
}
import SwiftUI
struct CourseView: View {
// #ObservedObject allows us to update views whenever values in course change
#ObservedObject var course: Course
#EnvironmentObject var user: User
var body: some View {
NavigationView {
List {
NavigationLink(destination: WritingPromptView(prompt: "What is your course goal, \(user.first)?", explanationText: "This is the answer", textLocation: self.$course.goal)) {
Text("Course Goal")
}
NavigationLink(destination: NotepadView(parent: self.course)) {
Text("Notepad")
}
NavigationLink(destination: WritingPromptView(prompt: "<Reflection prompt goes here>", explanationText: "<How to reflect goes here>", textLocation: self.$course.reflection)) {
Text("Reflection")
}
}.navigationBarTitle(course.name)
}
}
}
It's a double NavigationBar. Just remove NavigationView from your CourseView. If you have Previews for CourseView, you will probably want to wrap it NavigationView there.