Remove specific screen from NavigationView in SwiftUI - swiftui

Currently i am using NavigationView->NavigationLink to navigate one screen to another. How can i remove specific screen from NavigationView?
For Example I have four screen A, B, C and D. The navigation chain like this A->B-C->D. From Screen D how can i go back in Screen B and then Sceen A

To achieve this, you need to the new NavigationStack for iOS 16 which allows programmatic navigation.
The NavigationStack takes a path parameter; and array that you add or remove objects to, representing your navigation stack. Use the navigationDestination modifier to decide which actual views to push on the stack based on the contents of the path.
Example:
enum NavView {
case b, c, d
}
struct ContentView: View {
#State private var path: [NavView] = []
var body: some View {
NavigationStack(path: $path) {
VStack {
NavigationLink("Show B", value: NavView.b)
.navigationTitle("View A")
}
.navigationDestination(for: NavView.self) { view in
switch view {
case .b: ViewB(path: $path)
case .c: ViewC(path: $path)
case .d: ViewD(path: $path)
}
}
}
}
}
struct ViewB: View {
#Binding var path: [NavView]
var body: some View {
NavigationLink("Show C", value: NavView.c)
.navigationTitle("View B")
}
}
struct ViewC: View {
#Binding var path: [NavView]
var body: some View {
Button("Show D", action: { path.append(NavView.d) })
.navigationTitle("View C")
}
}
struct ViewD: View {
#Binding var path: [NavView]
var body: some View {
Button("Back to B", action: { path.removeLast(2) })
.navigationTitle("View D")
}
}
Note that the navigationDestination modifier is not on the NavigationStack, but one one of its contained views.

For this you can use either NavigationView and NavigationLink or the new NavigationStack. (I am not familiar yet with the latter but I can explain the general concept).
If you use the standard NavigationLink it will use a initialiser with a destination and a label. With this, you are limiting yourself as you are putting the view in control of the navigation. The view will push and pop and you are not in the loop so you can't achieve what you want.
However, you can use a different initialiser to create the NavigationLink that uses a binding to a Bool that determines if the navigation link is active (i.e. if it is pushed).
https://developer.apple.com/documentation/swiftui/navigationlink/init(isactive:destination:label:)
(This is deprecated in iOS16 though so it depends on what iOS version you are targeting).
With this you can now use the value of the isActive boolean to control the push of the navigation link. So if you have a boolean in your code you can update it and it will make the app pop back to the view you want to.

Related

SwiftUI Navigation: why is Timer.publish() in View body breaking nav stack

SwiftUI n00b here. I'm trying some very simple navigation using NavigationView and NavigationLink. In the sample below, I've isolated to a 3 level nav. The 1st level is just a link to the 2nd, the 2nd to the 3rd, and the 3rd level is a text input box.
In the 2nd level view builder, I have a
private let timer = Timer.publish(every: 2, on: .main, in: .common)
and when I navigate to the 3rd level, as soon as I start typing into the text box, I get navigated back to the 2nd level.
Why?
A likely clue that I don't understand. The print(Self._printChanges()) in the 2nd level shows
NavLevel2: #self changed.
immediately when I start typing into the 3rd level text box.
When I remove this timer declaration, the problem goes away. Alternatively, when I modify the #EnvironmentObject I'm using in the 3rd level to just be #State, the problem goes away.
So trying to understand what's going on here, if this is a bug, and if it's not a bug, why does it behave this way.
Here's the full ContentView building code that repos this
import SwiftUI
class AuthDataModel: ObservableObject {
#Published var someValue: String = ""
}
struct NavLevel3: View {
#EnvironmentObject var model: AuthDataModel
var body: some View {
print(Self._printChanges())
return TextField("Level 3: Type Something", text: $model.someValue)
// Replacing above with this fixes everything, even when the
// below timer is still in place.
// (put this decl instead of #EnvironmentObject above
// #State var fff: String = ""
// )
// return TextField("Level 3: Type Something", text: $fff)
}
}
struct NavLevel2: View {
// LOOK HERE!!!! Removing this declaration fixes everything.
private let timer = Timer.publish(every: 2, on: .main, in: .common)
var body: some View {
print(Self._printChanges())
return NavigationLink(
destination: NavLevel3()
) { Text("Level 2") }
}
}
struct ContentView: View {
#StateObject private var model = AuthDataModel()
var body: some View {
print(Self._printChanges())
return NavigationView {
NavigationLink(destination: NavLevel2())
{
Text("Level 1")
}
}
.environmentObject(model)
}
}
First, if you remove #StateObject from model declaration in ContentView, it will work.
You should not set the whole model as a State for the root view.
If you do, on each change of any published property, your whole hierarchy will be reconstructed. You will agree that if you type changes in the text field, you don't want the complete UI to rebuild at each letter.
Now, about the behaviour you describe, that's weird.
Given what's said above, it looks like when you type, the whole view is reconstructed, as expected since your model is a #State object, but reconstruction is broken by this unmanaged timer.. I have no real clue to explain it, but I have a rule to avoid it ;)
Rule:
You should not make timers in view builders. Remember swiftUI views are builders and not 'views' as we used to represent before. The concrete view object is returned by the 'body' function.
If you put a break on timer creation, you will notice your timer is called as soon as the root view is displayed. ( from NavigationLink(destination: NavLevel2())
That's probably not what you expect.
If you move your timer creation in the body, it will work, because the timer is then created when the view is created.
var body: some View {
var timer = Timer.publish(every: 2, on: .main, in: .common)
print(Self._printChanges())
return NavigationLink(
destination: NavLevel3()
) { Text("Level 2") }
}
However, it is usually not the right way neither.
You should create the timer:
in the .appear handler, keep the reference,
and cancel the timer in .disappear handler.
in a .task handler that is reserved for asynchronous tasks.
I personally only declare wrapped values ( #State, #Binding, .. ) in view builders structs, or very simple primitives variables ( Bool, Int, .. ) that I use as conditions when building the view.
I keep all functional stuffs in the body or in handlers.
To stop going back to the previous view when you type in the TextField add .navigationViewStyle(.stack) to the NavigationView
in ContentView.
Here is the code I used to test my answer:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#StateObject var model = AuthDataModel()
var body: some View {
NavigationView {
NavigationLink(destination: NavLevel2()){
Text("Level 1")
}
}.navigationViewStyle(.stack) // <--- here the important bit
.environmentObject(model)
}
}
class AuthDataModel: ObservableObject {
#Published var someValue: String = ""
}
struct NavLevel3: View {
#EnvironmentObject var model: AuthDataModel
var body: some View {
TextField("Level 3: Type Something", text: $model.someValue)
}
}
struct NavLevel2: View {
#EnvironmentObject var model: AuthDataModel
#State var tickCount: Int = 0 // <-- for testing
private let timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
var body: some View {
NavigationLink(destination: NavLevel3()) {
Text("Level 2 tick: \(tickCount)")
}
.onReceive(timer) { val in // <-- for testing
tickCount += 1
}
}
}

How to change values inside a picker using #Published property wrapper in swiftui

hi am having issues with the picker view in swiftui
i have created one file with just a class like this
import Foundation
import SwiftUI
class something: ObservableObject {
#Published var sel = 0
}
and then I created 2 views
import SwiftUI
struct ContentView: View {
#StateObject var hihi: something
var characters = ["Makima", "Ryuk", "Itachi", "Gojou", "Goku", "Eren", "Levi", "Jiraya", "Ichigo", "Sukuna"]
var body: some View {
VStack {
Section{
Picker("Please choose a character", selection: $hihi.sel) {
ForEach(characters, id: \.self) { name in
Text(name)
}
}
Text(characters[hihi.sel])
}
now(hihi: something())
}
}
}
struct now: View {
#StateObject var hihi: something
var body: some View {
Text("\(hihi.sel)")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(hihi: something())
}
}
now the problem am facing is that the code compiles but the picker ain't working it won't change to any other value in the array I have provided it recoils back to its original value provided that is 0th index "Makima" and it won't select any other option, why so?
please help
There are three problems, the main one being the mismatching selection.
In the Picker, your selection is based on the string value for each character. This is because the ForEach identifies each Text by the name string, since you used id: \.self.
However, your something model (which ideally should start with a capital letter by convention) has a numeric selection. Because the items in the Picker have String IDs, and this is an Int, the selection can't be set.
You can change your model to this:
class something: ObservableObject {
#Published var sel = "Makima"
}
Which also requires a slight change in the body:
VStack {
Section{
Picker("Please choose a character", selection: $hihi.sel) {
ForEach(characters, id: \.self) { name in
Text(name)
}
}
Text(hihi.sel) // Now getting string directly
}
now(hihi: something())
}
Notice we now have two views showing the selected character - but only the top one updates. The bottom one may now be redundant (the now view), but I'll show you how you can get it working anyway. This is where we encounter the 2nd problem:
You are creating a new instance of something() when passing it to now (again, should start with a capital). This means that the current instance of hihi stored in ContentView is not passed along. You are just creating a new instance of something, which uses the default value. This is completely independent from the hihi instance.
Replace:
now(hihi: something())
With:
now(hihi: hihi)
The final problem, which may not be as visible, is that you shouldn't be using #StateObject in now, since it doesn't own the object/data. Instead, the object is passed in, so you should use #ObservedObject instead. Although the example now works even without this change, you will have issues later on when trying to change the object within the now view.
Replace #StateObject in now with #ObservedObject.
Full answer (something is initialized in ContentView only for convenience of testing):
struct ContentView: View {
#StateObject var hihi: something = something()
var characters = ["Makima", "Ryuk", "Itachi", "Gojou", "Goku", "Eren", "Levi", "Jiraya", "Ichigo", "Sukuna"]
var body: some View {
VStack {
Section{
Picker("Please choose a character", selection: $hihi.sel) {
ForEach(characters, id: \.self) { name in
Text(name)
}
}
Text(hihi.sel)
}
now(hihi: hihi)
}
}
}
struct now: View {
#ObservedObject var hihi: something
var body: some View {
Text(hihi.sel)
}
}
class something: ObservableObject {
#Published var sel = "Makima"
}

Remove screen from navigation stack in SwiftUI

I'm using NavigationLink to navigate screens in NavigationView.
How can I remove screen from navigation stack?
Not just hide the navigation "Back" button but completely remove screen from stack?
For example, I have screen chain like this: A -> B -> C
How can I remove screen B to go back from C to A?
Or, another example, how to remove screen A and B, so the C screen will be the root?
Or all of this is impossible by conception of SwiftUI?
In terms of your first question (going from C to A), this is often called "popping to root" and has a number of solutions here on SO, including: https://stackoverflow.com/a/59662275/560942
Your second question (replacing A with C as the root view) is a little different. You can do that by replacing A with C in the view hierarchy. In order to do this, you'd need to have some way to communicate with the parent view -- I chose a simple #State/#Binding to do this, but one could use an ObservableObject or even callback function instead.
enum RootView {
case A, C
}
struct ContentView : View {
#State private var rootView : RootView = .A
var body: some View {
NavigationView {
switch rootView {
case .A:
ViewA(rootView: $rootView)
case .C:
ViewC(rootView: $rootView)
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct ViewA : View {
#Binding var rootView : RootView
var body: some View {
VStack {
Text("View A")
NavigationLink(destination: ViewB(rootView: $rootView)) {
Text("Navigate to B")
}
}
}
}
struct ViewB : View {
#Binding var rootView : RootView
var body: some View {
VStack {
Text("View B")
NavigationLink(destination: ViewC(rootView: $rootView)) {
Text("Navigate to C")
}
Button(action: {
rootView = .C
}) {
Text("Navigate to C as root view")
}
}
}
}
struct ViewC : View {
#Binding var rootView : RootView
var body: some View {
VStack {
Text("View C")
switch rootView {
case .A:
Button(action: {
rootView = .C
}) {
Text("Switch this to the root view")
}
case .C:
Text("I'm the root view")
}
}
}
}
There is a new element in SwiftUI called NavigationStack which lets you manipulate the stack any way you want. I have been playing with it in an example project attempting to use it in a maintainable and programatic approach to coordinators in SwiftUI

SwiftUI: Navigate from Sheet to a new View

How can I push a new View on the navigation stack from within a Sheet. I want to display a list of Lessons. When tabbing on one of the lessons, a sheet should open showing details about the lesson. From within the Sheet one should be able to start the lesson in a new fullscreen view.
import SwiftUI
struct ContentView: View {
var lessons = [Lesson(id:"1"), Lesson(id:"2"), Lesson(id:"3"), Lesson(id:"4"), Lesson(id:"5"), Lesson(id:"6"), Lesson(id:"7"), Lesson(id:"8"), Lesson(id:"9")]
var body: some View {
NavigationView(){
Form{
List(lessons){ lesson in
LessonButton(lesson: lesson)
}
}
}
}
}
struct LessonButton:View{
#State var showSheet = false
var lesson:Lesson
var body: some View {
Button(action:{self.showSheet = true}){
Text(lesson.name)
}.sheet(isPresented:$showSheet){
NavigationLink(destination: Text("reached")){
Text("start")
}
}
}
}
struct Lesson: Identifiable{
var id:String
var name: String{
"Lesson \(self.id)"
}
}
However the NavigationLink is not working. I guess, this is because the Sheet is not a ChildView of Content View. That's probably why it does not work. But how can it be achieved?
A bit late, but this question came up while solving this. Your sheet acts like its own view controller stack. You can't navigate the parent through the sheet overlay, nor should you. It does seem like you're asking what I was looking for, which is to emulate other apple apps that navigate in sheets. You simply need an additional NavigationView within your sheet. This will give you a navigation stack to push other sheet styled views to the navigation controller within your first sheet.
(SwiftUI beginner, verbiage is likely wrong)
import SwiftUI
struct NavigateFromSheet: View {
var lessons = [Lesson(id:"1"), Lesson(id:"2"), Lesson(id:"3"), Lesson(id:"4"), Lesson(id:"5"), Lesson(id:"6"), Lesson(id:"7"), Lesson(id:"8"), Lesson(id:"9")]
var body: some View {
NavigationView(){
Form {
List(lessons){ lesson in
LessonButton(lesson: lesson)
}
}
}
}
}
struct LessonButton:View{
#State var showSheet = false
var lesson:Lesson
var body: some View {
Button(action:{self.showSheet = true}){
Text(lesson.name)
}.sheet(isPresented: $showSheet){
NavigationView {
VStack {
Text("My First Sheet")
NavigationLink(destination: Text("reached")){
Text("My Second Sheet")
}
}
}
}
}
}
struct Lesson: Identifiable{
var id:String
var name: String{
"Lesson \(self.id)"
}
}
struct NavigateFromSheet_Previews: PreviewProvider {
static var previews: some View {
NavigateFromSheet()
}
}
Sheet is modal view mode, you can enter in it and return back from it.
Actually I can't understand why do you need a sheet in described scenario. As you described it is expected:
List -> Details -> Lesson,
so use consequently two navigation links, one in List, one in Details. This is a native Apple design for NavigationView/NavigationLink usage - navigation from view to view.

Present a View Non-Modally

I am creating a sign in page for my app and would like to present the home screen in a way that the user can not go back. In Swift UI how do I present it so the new view does not present in a card like style? I know this presenting style is now default for iOS 13.
This is what I already have.
import SwiftUI
struct Test : View {
var body: some View {
PresentationButton(Text("Click to show"), destination: Extra() )
}
}
I would like the view to present full screen.
Use a NavigationView with a NavigationButton and hide the destination view's navigation bar's back button.
For example:
struct ContentView : View {
let destinationView = Text("Destination")
.navigationBarItem(title: Text("Destination View"), titleDisplayMode: .automatic, hidesBackButton: true)
var body: some View {
NavigationView {
NavigationButton(destination: destinationView) {
Text("Tap Here")
}
}
}
}
You can also disable the destination view's navigation bar altogether by doing let destinationView = Text("Destination").navigationBarHidden(true).