SwiftUI's new NavigationStack does not navigate to next screen - ios

I am trying to integrate NavigationStack in my SwiftUI app, I have three views CealUIApp, OnBoardingView and UserTypeView. I just want to navigate from OnBoardingView to UserTypeView when user presses a button in OnBoardingView
Below is my code for CealUIApp
#main
struct CealUIApp: App {
#State private var path = [String]()
var body: some Scene {
WindowGroup {
NavigationStack(path: $path){
OnBoardingView(path: $path)
}.navigationDestination(for: String.self) { string in
UserTypeView()
}
}
}
}
Now in OnBoardingView, the button code is as follows
Button {
path.append("UserTypeView")
} label: {
Text("Hello")
}
As soon as I press the button I am not navigated to UserTypeView, instead I just see a white screen with a warning icon at the centre

I just want to navigate from OnBoardingView to UserTypeView when user presses a button in OnBoardingView,
then try this approach, where .navigationDestination(...) is moved to the OnBoardingView,
as shown in this example code:
#main
struct CealUIApp: App {
#State var path = [String]()
var body: some Scene {
WindowGroup {
NavigationStack(path: $path) {
OnBoardingView(path: $path)
}
}
}
}
struct OnBoardingView: View {
#Binding var path: [String]
var body: some View {
Button {
path.append("UserTypeView")
} label: {
Text("go to UserTypeView")
}
.navigationDestination(for: String.self) { string in
UserTypeView()
}
}
}
struct UserTypeView: View {
var body: some View {
Text("UserTypeView")
}
}

Related

Why does my SwiftUI app jump back to the previous view?

I created an iOS app with SwiftUI and Swift. The purpose is as follows:
User enters an address into an input field (AddressInputView)
On submit, the app switches to ResultView and passes the address
Problem: The ResultView is visible for just a split second and then suddenly jumps back to AddressInputView.
Question: Why is the app jumping back to the previous view (Instead of staying in ResultView) and how can the code be adjusted to fix the issue?
My Code:
StartView.swift
struct StartView: View {
var body: some View {
NavigationStack {
AddressInputView()
}
}
}
AddressInputView.swift
enum Destination {
case result
}
struct AddressInputView: View {
#State private var address: String = ""
#State private var experiences: [Experience] = []
#State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
VStack {
TextField("", text: $address, prompt: Text("Search address"))
.onSubmit {
path.append(Destination.result)
}
Button("Submit") {
path.append(Destination.result)
}
.navigationDestination(for: Destination.self, destination: { destination in
switch destination {
case .result:
ResultView(address: $address, experiences: $experiences)
}
})
}
}
}
}
ExperienceModels.swift
struct ExperienceServiceResponse: Codable {
let address: String
let experiences: [Experience]
}
ResultView.swift
struct ResultView: View {
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
#Binding private var address: String
#Binding private var experiences: [Experience]
init(address: Binding<String>, experiences: Binding<[Experience]>) {
_address = Binding(projectedValue: address)
_experiences = Binding(projectedValue: experiences)
}
var body: some View {
NavigationStack {
ScrollView {
VStack(alignment: .leading) {
Text("Results")
ForEach(experiences, id: \.name) { experience in
ResultTile(experience: experience)
}
}
}
}
}
}
You have too many NavigationStacks. This is a container view that should be containing your view hierarchy, not something that is declared at every level. So, amend your start view:
struct StartView: View {
#State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
AddressInputView(path: $path)
}
}
}
AddressInputView should take the path as a binding:
#Binding var path: NavigationPath
And you should remove NavigationStack from the body of that view. Likewise with ResultView. I made these changes in a project using the code you posted and the issue was fixed.
I'm not sure exactly why your view is popping back but you're essentially pushing a navigation view onto a navigation view onto a navigation view, so it's not entirely surprising the system gets confused.

How to send extra data using NavigationStack with SwiftUI?

I have three views A,B and C. User can navigate from A to B and from A to C. User can navigate from B to C. Now I want to differentiate if the user have come from A to C or from B to C so I was looking in how to pass extra data in NavigationStack which can help me differentiate
Below is my code
import SwiftUI
#main
struct SampleApp: App {
#State private var path: NavigationPath = .init()
var body: some Scene {
WindowGroup {
NavigationStack(path: $path){
A(path: $path)
.navigationDestination(for: ViewOptions.self) { option in
option.view($path)
}
}
}
}
enum ViewOptions {
case caseB
case caseC
#ViewBuilder func view(_ path: Binding<NavigationPath>) -> some View{
switch self{
case .caseB:
B(path: path)
case .caseC:
C(path: path)
}
}
}
}
struct A: View {
#Binding var path: NavigationPath
var body: some View {
VStack {
Text("A")
Button {
path.append(SampleApp.ViewOptions.caseB)
} label: {
Text("Go to B")
}
Button {
path.append(SampleApp.ViewOptions.caseC)
} label: {
Text("Go to C")
}
}
}
}
struct B: View {
#Binding var path: NavigationPath
var body: some View {
VStack {
Text("B")
Button {
path.append(SampleApp.ViewOptions.caseC)
} label: {
Text("Go to C")
}
}
}
}
struct C: View {
#Binding var path: NavigationPath
var body: some View {
VStack {
Text("C")
}
}
}
Instead of "pass extra data in NavigationStack" you can pass data in a NavigationRouter. It gives you much more control
#available(iOS 16.0, *)
//Simplify the repetitive code
typealias NavSource = SampleApp.ViewOptions
#available(iOS 16.0, *)
struct NavigationRouter{
var path: [NavSource] = .init()
///Adds the provided View to the stack
mutating func goTo(view: NavSource){
path.append(view)
}
///Searches the stack for the `View`, if the view is `nil`, the stack returns to root, if the `View` is not found the `View` is presented from the root
mutating func bactrack(view: NavSource?){
guard let view = view else{
path.removeAll()
return
}
//Look for the desired view
while !path.isEmpty && path.last != view{
path.removeLast()
}
//If the view wasn't found add it to the stack
if path.isEmpty{
goTo(view: view)
}
}
///Identifies the previous view in the stack, returns nil if the previous view is the root
func identifyPreviousView() -> NavSource?{
//1 == current view, 2 == previous view
let idx = path.count - 2
//Make sure idx is valid index
guard idx >= 0 else{
return nil
}
//return the view
return path[idx]
}
}
Once you have access to the router in the Views you can adjust accordingly.
#available(iOS 16.0, *)
struct SampleApp: View {
#State private var router: NavigationRouter = .init()
var body: some View {
NavigationStack(path: $router.path){
A(router: $router)
//Have the root handle the type
.navigationDestination(for: NavSource.self) { option in
option.view($router)
}
}
}
//Create an `enum` so you can define your options
//Conform to all the required protocols
enum ViewOptions: Codable, Equatable, Hashable{
case caseB
case caseC
//If you need other arguments add like this
case unknown(String)
//Assign each case with a `View`
#ViewBuilder func view(_ path: Binding<NavigationRouter>) -> some View{
switch self{
case .caseB:
B(router: path)
case .caseC:
C(router: path)
case .unknown(let string):
Text("View for \(string.description) has not been defined")
}
}
}
}
#available(iOS 16.0, *)
struct A: View {
#Binding var router: NavigationRouter
var body: some View {
VStack{
Button {
router.goTo(view: .caseB)
} label: {
Text("To B")
}
Button {
router.goTo(view: .caseC)
} label: {
Text("To C")
}
}.navigationTitle("A")
}
}
#available(iOS 16.0, *)
struct B: View {
#Binding var router: NavigationRouter
var body: some View {
VStack{
Button {
router.goTo(view: .caseC)
} label: {
Text("Hello")
}
}.navigationTitle("B")
}
}
#available(iOS 16.0, *)
struct C: View {
#Binding var router: NavigationRouter
//Identify changes based on previous View
var fromA: Bool{
//nil is the root
router.identifyPreviousView() == nil
}
var body: some View {
VStack{
Text("Welcome\(fromA ? " Back" : "" )")
Button {
//Append to the path the enum value
router.bactrack(view: router.identifyPreviousView())
} label: {
Text("Back")
}
Button {
//Append to the path the enum value
router.goTo(view: .unknown("\"some other place\""))
} label: {
Text("Next")
}
}.navigationTitle("C")
.navigationBarBackButtonHidden(true)
}
}
You can read the second-to-last item in the path property to learn what the previous screen was.
To do this, it's easier to use an actual array of ViewOptions as the path, instead of a NavigationPath.
For example:
struct SampleApp: App {
// Use your own ViewOptions enum, instead of NavigationPath
#State private var path: [ViewOptions] = []
var body: some Scene {
WindowGroup {
NavigationStack(path: $path){
A(path: $path)
.navigationDestination(for: ViewOptions.self) { option in
option.view($path)
}
}
}
}
}
struct C: View {
#Binding var path: [ViewOptions]
var previousView: ViewOptions? {
path
.suffix(2) // Get the last 2 elements of the path
.first // Get the first of those last 2 elements
}
var body: some View {
VStack {
Text("C")
}
}
}
Remember, a NavigationPath is nothing more than a type-erased array. It can be used to build a NavigationStack quickly without having to worry that all destination values have to match the same type. Since as you're controlling the navigation flow with your own type ViewOptions, it makes no sense to use NavigationPath.

Dismissing a view with SwiftUI

I want to show an instruction page the first time a user opens my app. I have gotten this to work, but I cannot understand how to get back to the contentView (without restarting the app).
Just to clarify: I want the "Dismiss this view" button on InstructionView to set the view to be shown to "ContentView" and to dismiss the InstructionView. As it is now the viewRouter.setToContentView() makes the App crash, and I cannot get around it.
Thanks in advance
TestDismissApp
import SwiftUI
#main
struct TestDismissApp: App {
var body: some Scene {
WindowGroup {
MotherView()
}
}
}
MotherView
import SwiftUI
struct MotherView : View {
#ObservedObject var viewRouter = ViewRouter()
var body: some View {
VStack {
if viewRouter.currentPage == "InstructionView" {
InstructionView()
} else if viewRouter.currentPage == "ContentView" {
ContentView()
}
}
}
}
ViewRouter
import Foundation
class ViewRouter: ObservableObject {
init() {
//UserDefaults.standard.set(false, forKey: "didLaunchBefore") remove // if you want to show the instructions again for test reasons
if !UserDefaults.standard.bool(forKey: "didLaunchBefore") {
UserDefaults.standard.set(true, forKey: "didLaunchBefore")
currentPage = "InstructionView"
} else {
currentPage = "ContentView"
}
}
func setToContentView () {
currentPage = "ContentView"
}
#Published var currentPage: String
}
ContentView
import SwiftUI
struct ContentView: View {
var body: some View {
Text("My wonderful content")
.padding()
}
}
InstructionView
import SwiftUI
struct InstructionView: View {
#Environment(\.dismiss) var dismiss
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
Text("Instruction: This app shows a wonderful sentence")
Button {
viewRouter.setToContentView()
dismiss()
} label: {
Text("Dismiss this view")
}
}
}
}
try this:
in MotherView use #StateObject var viewRouter = ViewRouter() and
InstructionView().environmentObject(viewRouter)

How to run child function from parent?

I want to call childFunction() demo ChildView by pressing the button in the parent view.
import SwiftUI
struct ChildView: View {
func childFunction() {
print("I am the child")
}
var body: some View {
Text("I am the child")
}
}
struct ContentView: View {
var function: (() -> Void)?
var body: some View {
ChildView()
Button(action: {
self.function!()
}, label: {
Text("Button")
})
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Update:
Thanks #RajaKishan, it works, but I need it working also recursively
import SwiftUI
struct ContentView: View {
#State var text: String = "Parent"
var isNavigationViewAvailable = true
func function() {
print("This view is \(text)")
}
var body: some View {
VStack {
if isNavigationViewAvailable {
Button(action: {
function()
}, label: {
Text("Button")
})
}
if isNavigationViewAvailable {
NavigationView {
List {
NavigationLink("Child1") {
ContentView(text: "Child1", isNavigationViewAvailable: false)
}
NavigationLink("Child2") {
ContentView(text: "Child2", isNavigationViewAvailable: false)
}
NavigationLink("Child3") {
ContentView(text: "Child3", isNavigationViewAvailable: false)
}
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Maybe is is not the best looking example, but the question is, how to force the button to run function of it's child after user visited corresponding child.
Like, on start when user presses the button it prints "This view is Parent". After user comes to child1 the button press should print "This view is Child1" as so on. So, the function that button runs should be referenced from the last child.
Update:
In the end I wrote this solution.
Update:
I received feedback, asking me for clarification. No problem. I hope it'll help somebody.:)
Clarification:
I did not enclose the whole code, just used a simple example. But I needed this in my implementation of a tree-like generated menu: when each item in the menu has or does not have its children. Pressing on parent object user comes into child objects. And here I needed to be able to come back from a child object to parent, but call this dismiss function from the parent object. For this I used the following code and referred to this function to each parent object:
#Environment(\.presentationMode) var presentationMode
presentationMode.wrappedValue.dismiss()
You can create an object for a ChildView.
struct ChildView: View {
func childFunction() {
print("I am the child")
}
var body: some View {
Text("I am the child")
}
}
struct ContentView: View {
let childView = ChildView()
var body: some View {
childView
Button(action: {
childView.childFunction()
}, label: {
Text("Button")
})
}
}
EDIT : For the list, you can use the array of the model and call the destination function by index.
Here is the simple child-parent example.
struct ChildView: View {
var text: String
func childFunction() {
print("This view is \(text)")
}
var body: some View {
Text("I am the child")
}
}
struct ContentView55: View {
#State private var arrData = [Model(title: "Child1", destination: ChildView(text: "Child1")),
Model(title: "Child2", destination: ChildView(text: "Child2")),
Model(title: "Child3", destination: ChildView(text: "Child3"))]
var body: some View {
VStack {
Button(action: {
arrData[1].destination.childFunction()
}, label: {
Text("Button")
})
NavigationView {
SwiftUI.List(arrData) {
NavigationLink($0.title, destination: $0.destination)
}
}
}
}
}
struct Model: Identifiable {
var id = UUID()
var title: String
var destination: ChildView
}
Note: You need to index for the row to call child function.

How to notify view that the variable state has been updated from a extracted subview in SwiftUI

I have a view that contain users UsersContentView in this view there is a button which is extracted as a subview: RequestSearchButton(), and under the button there is a Text view which display the result if the user did request to search or no, and it is also extracted as a subview ResultSearchQuery().
struct UsersContentView: View {
var body: some View {
ZStack {
VStack {
RequestSearchButton()
ResultSearchQuery(didUserRequestSearchOrNo: .constant("YES"))
}
}
}
}
struct RequestSearchButton: View {
var body: some View {
Button(action: {
}) {
Text("User requested search")
}
}
}
struct ResultSearchQuery: View {
#Binding var didUserRequestSearchOrNo: String
var body: some View {
Text("Did user request search: \(didUserRequestSearchOrNo)")
}
}
How can I update the #Binding var didUserRequestSearchOrNo: String inside the ResultSearchQuery() When the button RequestSearchButton() is clicked. Its so confusing!
You need to track the State of a variable (which is indicating if a search is active or not) in your parent view, or your ViewModel if you want to extract the Variables. Then you can refer to this variable in enclosed child views like the Search Button or Search Query Results.
In this case a would prefer a Boolean value for the tracking because it's easy to handle and clear in meaning.
struct UsersContentView: View {
#State var requestedSearch = false
var body: some View {
ZStack {
VStack {
RequestSearchButton(requestedSearch: $requestedSearch)
ResultSearchQuery(requestedSearch: $requestedSearch)
}
}
}
}
struct RequestSearchButton: View {
#Binding var requestedSearch: Bool
var body: some View {
Button(action: {
requestedSearch.toggle()
}) {
Text("User requested search")
}
}
}
struct ResultSearchQuery: View {
#Binding var requestedSearch: Bool
var body: some View {
Text("Did user request search: \(requestedSearch.description)")
}
}
Actually I couldn't understand why you used two struct which are connected to eachother, you can do it in one struct and Control with a state var
struct ContentView: View {
var body: some View {
VStack {
RequestSearchButton()
}
}
}
struct RequestSearchButton: View {
#State private var clicked : Bool = false
var body: some View {
Button(action: {
clicked = true
}) {
Text("User requested search")
}
Text("Did user request search: \(clicked == true ? "YES" : "NO")")
}
}
if this is not what you are looking for, could you make a detailed explain.

Resources