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

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.

Related

SwiftUI - "Argument passed to call that takes no arguments"?

I have an issue with the coding for my app, where I want to be able to scan a QR and bring it to the next page through navigation link. Right now I am able to scan a QR code and get a link but that is not a necessary function for me. Below I attached my code and got the issue "Argument passed to call that takes no arguments", any advice or help would be appreciated :)
struct QRCodeScannerExampleView: View {
#State private var isPresentingScanner = false
#State private var scannedCode: String?
var body: some View {
VStack(spacing: 10) {
if let code = scannedCode {
//error below
NavigationLink("Next page", destination: PageThree(scannedCode: code), isActive: .constant(true)).hidden()
}
Button("Scan Code") {
isPresentingScanner = true
}
Text("Scan a QR code to begin")
}
.sheet(isPresented: $isPresentingScanner) {
CodeScannerView(codeTypes: [.qr]) { response in
if case let .success(result) = response {
scannedCode = result.string
isPresentingScanner = false
}
}
}
}
}
Page Three Code
import SwiftUI
struct PageThree: View {
var body: some View {
Text("Hello, World!")
}
}
struct PageThree_Previews: PreviewProvider {
static var previews: some View {
PageThree()
}
}
You forgot property:
struct PageThree: View {
var scannedCode: String = "" // << here !!
var body: some View {
Text("Code: " + scannedCode)
}
}
You create your PageThree View in two ways, One with scannedCode as a parameter, one with no params.
PageThree(scannedCode: code)
PageThree()
Meanwhile, you defined your view with no initialize parameters
struct PageThree: View {
var body: some View {
Text("Hello, World!")
}
}
For your current definition, you only can use PageThree() to create your view. If you want to pass value while initializing, change your view implementation and consistently using one kind of initializing method.
struct PageThree: View {
var scannedCode: String
var body: some View {
Text(scannedCode)
}
}
or
struct PageThree: View {
private var scannedCode: String
init(code: String) {
scannedCode = code
}
var body: some View {
Text(scannedCode)
}
}
This is basic OOP, consider to learn it well before jump-in to development.
https://docs.swift.org/swift-book/LanguageGuide/Initialization.html

Updating a binding value pops back to the parent view in the navigation stack

I am passing a Person binding from the first view to the second view to the third view, when I update the binding value in the third view it pops back to the second view, I understand that SwiftUI updates the views that depend on the state value, but is poping the current view is the expected behavior or I am doing something wrong?
struct Person: Identifiable {
let id = UUID()
var name: String
var numbers = [1, 2]
}
struct FirstView: View {
#State private var people = [Person(name: "Current Name")]
var body: some View {
NavigationView {
List($people) { $person in
NavigationLink(destination: SecondView(person: $person)) {
Text(person.name)
}
}
}
}
}
struct SecondView: View {
#Binding var person: Person
var body: some View {
Form {
NavigationLink(destination: ThirdView(person: $person)) {
Text("Update Info")
}
}
}
}
struct ThirdView: View {
#Binding var person: Person
var body: some View {
Form {
Button(action: {
person.numbers.append(3)
}) {
Text("Append a new number")
}
}
}
}
When navigating twice you need to either use isDetailLink(false) or StackNavigationViewStyle, e.g.
struct FirstView: View {
#State private var people = [Person(name: "Current Name")]
var body: some View {
NavigationView {
List($people) { $person in
NavigationLink(destination: SecondView(person: $person)) {
Text(person.name)
}
.isDetailLink(false) // option 1
}
}
.navigationViewStyle(.stack) // option 2
}
}
SwiftUI works by updating the rendered views to match what you have in your state.
In this case, you first have a list that contains an element called Current Name. Using a NavigationLink you select this item.
You update the name and now that previous element no longer exists, it's been replaced by a new element called New Name.
Since Current Name no longer exists, it also cannot be selected any longer, and the view pops back to the list.
To be able to edit the name without popping back, you'll need to make sure that the item on the list is the same, even if the name has changed. You can do this by using an Identifiable struct instead of a String.
struct Person: Identifiable {
let id = UUID().uuidString
var name = "Current Name"
}
struct ParentView: View {
#State private var people = [Person()]
var body: some View {
NavigationView {
List($people) { $person in
NavigationLink(destination: ChildView(person: $person)) {
Text(person.name)
}
}
}
}
}
struct ChildView: View {
#Binding var person: Person
var body: some View {
Button(action: {
person.name = "New Name"
}) {
Text("Update Name")
}
}
}

Importing the data typed by the user to a view?

In the process of making my first Finance App, I want the user to type their Credit Card Name and las four numbers (probably more info since this is a draft) into this Modally presented view, to then be seen in a cards index, widget-look-like.
struct CardListView: View {
#State var isPresentingAddModal = false
#State var emisorTarjeta = ""
#State var numeroTarjeta = ""
var headerView: some View {
HStack {
Text("Tus tarjetas")
Spacer()
Button("Añadir nueva") {
self.isPresentingAddModal.toggle()
}
.sheet(isPresented: $isPresentingAddModal, content: {
HStack {
Text("Emisor de tarjeta")
TextField("Seleccionar emisor de tarjeta", text: $emisorTarjeta)
}
HStack {
Text("Número de tarjeta")
TextField("Escribí tu número de tarjeta", text: $numeroTarjeta)
}
Button(action: {
self.isPresentingAddModal.toggle()
print("\(self.emisorTarjeta)")
}, label: {
Text("Añadir")
})
Spacer()
})
}
The question now is how to pass the info typed from the two textFields, to the view where the cards will be created. The button "Añadir" currently works as a dismiss button instead of an add one, since I don't know how to create that.
(Also, a lot of code like paddings and backgroundColors have been erased to make it clearer to see)
Enitre view of the homeView
Where the "añadir" button is
there are several ways to do this. One simple way is to use "#State" and "#Binding" like this:
In "CardListView" use this:
#Binding var emisorTarjeta: String
#Binding var numeroTarjeta: String
and in the "CardViewCreator" use:
#State var emisorTarjeta = ""
#State var numeroTarjeta = ""
Another way is to use "ObservableObject", create a class like this:
class CardModel: ObservableObject {
#Published var emisorTarjeta = ""
#Published var numeroTarjeta = ""
}
In the your "CardViewCreator" or some parent view:
#StateObject var cardModel = CardModel()
and pass it to the "CardListView" like this:
struct CardListView: View {
#ObservedObject var cardModel: CardModel
...
}
You can also use "EnvironmentObject" in a similar way.
It all depends on your case. I recommend reading up on "ObservedObject"
and using that.
A really simple way of doing this is to pass in a closure to run when the add button is tapped. Here's an example, which also shows how to dismiss the presented sheet
import SwiftUI
struct Card: Identifiable {
let id = UUID()
let provider: String
let number: String
}
struct ContentView: View {
#State private var cards = [Card]()
#State private var showingSheet = false
var body: some View {
VStack {
List(cards, rowContent: CardView.init)
.padding(.bottom, 10)
Button("Add") {
showingSheet = true
}
.padding()
}
.sheet(isPresented: $showingSheet) {
AddSheet(completion: addCard)
}
}
func addCard(provider: String, number: String) {
let newCard = Card(provider: provider, number: number)
cards.append(newCard)
}
}
struct CardView: View {
let card: Card
var body: some View {
VStack(alignment: .leading) {
Text(card.provider)
Text(card.number)
}
}
}
struct AddSheet: View {
#Environment(\.presentationMode) var presentationMode
#State private var provider = ""
#State private var number = ""
let completion: (String, String) -> Void
var body: some View {
VStack {
TextField("Provider", text: $provider).padding()
TextField("Number", text: $number).padding()
Button("Add") {
completion(provider, number)
presentationMode.wrappedValue.dismiss()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If you want to actually save the information passed in the textfield you would have to save it somewhere and later fetch it when required But this is only if you want to be able to access the information passed into the cards index after you have closed down the application and opened it up once again.

Passing binding back to parent's parent view

I have 4 views.
Grandparent
Parent
Child
EditView
Grandparent has a navigation link to Parent, and Parent a navigation link to Child. Child has a button which initializes a #State variable, location (a class), from Grandparent, via a binding in Parent and a binding in Child. That same button also updates a #State variable, showEditView, from Grandparent (via bindings again), which shows the EditView.
There are 11 lines which are currently commented out. If they are commented out, the app throws a "Fatal error: Unexpectedly found nil..." error when I tap the button in the Child view. If either section is uncommented, the button does not throw an error.
It also works if I pass the bound location to the EditView, not property itself like I'm currently doing, where it is then wrapped as an #ObservedObject.
I don't understand what is going on here. The only thing I can of is that, when it's working, SwiftUI is updating the location property because it's used in the body. If that is the case, that seems to indicate that I have to include a hidden Text view of this property every time I want to do have properties passed around this way.
Grandparent
import SwiftUI
struct Grandparent: View {
#State var location: Location!
#State var showEditView: Bool = false
var body: some View {
NavigationView {
VStack{
NavigationLink(
destination: Parent(location: $location, showEditView: $showEditView)) {
Text("Navigate")
}
// // section 1
// if location != nil {
// Text(location.name)
// } else {
// Text("No location yet")
// }
}
// // section 2
// .navigationBarItems(trailing:
// Button("Edit"){
// showEditView = true
// }
// .disabled(location == nil)
// )
}
.padding()
.sheet(isPresented: $showEditView) {
EditView(placemark: location, dismiss: { showEditView = false })
}
}
}
Parent
import SwiftUI
struct Parent: View {
#Binding var location: Location!
#Binding var showEditView: Bool
var body: some View {
NavigationLink(
destination: Child(location: $location, showEditView: $showEditView),
label: {
Text("Child")
})
}
}
Child
import SwiftUI
struct Child: View {
#Binding var location: Location!
#Binding var showEditView: Bool
var body: some View {
Button("Make location") {
location = Location(name: "Lebanon")
showEditView = true
}
}
}
EditView
import SwiftUI
struct EditView: View {
#ObservedObject var placemark: Location
var dismiss: () -> Void
var body: some View {
NavigationView {
Text(placemark.name)
.navigationTitle("Edit place")
.navigationBarItems(trailing: Button("Done") { dismiss() })
}
}
}
Location
import Foundation
class Location: ObservableObject {
init(name: String) {
self.name = name
}
#Published var name: String
}
You probably shouldn't declare your Location as ! and then do nil checking on it. Using ! is sort of asking for a crash to happen. I think what you're encountering is a the sheet getting rendered before location is set. There aren't any guarantees about when in the run loop a #State variable gets set, so it's better to account for scenarios where it is nil (and definitely not using ! to force unwrap it).
Secondly, at least given the scenario you have here, you probably shouldn't be using a class for Location -- it should just be a struct.
Eventually, you are going to run into a little bit of complexity, because judging by your View's name, you want to edit the Location at some point. This becomes a little more tricky with an Optional, since things like TextField want non-optional values, but this can be solved in various was (see where I used nonNilBinding).
Something like this is definitely a more safe approach than what you're currently doing. It may not be exactly what you want, but hopefully it can get you on the right path.
struct Location {
var name : String
}
struct Grandparent: View {
#State var location: Location?
#State var showEditView: Bool = false
var body: some View {
NavigationView {
VStack{
NavigationLink(
destination: Parent(location: $location, showEditView: $showEditView)) {
Text("Navigate")
}
if let location = location {
Text(location.name)
} else {
Text("No location yet")
}
}
.padding()
.sheet(isPresented: $showEditView) {
EditView(placemark: $location, dismiss: { showEditView = false })
}
}
}
}
struct Parent: View {
#Binding var location: Location?
#Binding var showEditView: Bool
var body: some View {
NavigationLink(
destination: Child(location: $location, showEditView: $showEditView),
label: {
Text("Child")
})
}
}
struct Child: View {
#Binding var location: Location?
#Binding var showEditView: Bool
var body: some View {
Button("Make location") {
location = Location(name: "Lebanon")
showEditView = true
}
}
}
struct EditView: View {
#Binding var placemark: Location?
var dismiss: () -> Void
var nonNilBinding : Binding<Location> {
.init { () -> Location in
placemark ?? Location(name:"Default")
} set: { (newValue) in
placemark = newValue
}
}
var body: some View {
NavigationView {
TextField("Name", text: nonNilBinding.name)
.navigationTitle("Edit place")
.navigationBarItems(trailing: Button("Done") { dismiss() })
}
}
}

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