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

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.

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.

Infinite loop by using #Binding when passing data between views

High-level description:
There is a nested view problem when a state object is being passed through views. At the end of the deepest view in the hierarchy, the app is frozen and memory consumption is increasing continuously.
Use-case
Partners list → Partner detail → (Locations list) → Location detail
Code-snippets
class PartnerViewModel: ObservableObject {
#Published var partners: [Partner] = Partner.partners
}
This view is loaded into a TabView and a NavigationStack components in the parent class.
struct PartnerListView: View {
#StateObject var viewModel = PartnerViewModel()
var body: some View {
List($viewModel.partners, id: \.self) { $partner in
NavigationLink {
PartnerDetailView(partner: $partner)
} label: {
Text(partner.name)
}
}
}
}
struct PartnerDetailView: View {
#Binding var partner: Partner
var body: some View {
Form {
Section("Locations") {
List($partner.locations, id: \.self) { $location in
NavigationLink {
LocationDetailView(location: $location)
} label: {
Text(location.name)
}
}
}
}
}
}
struct LocationDetailView: View {
#Binding var location: Location
var body: some View {
TextField("Name", text: $location.name)
}
}
The following snippets are workaround and it works but it might be temporary because I don't understand why the first attempt doesn't work and why this one does. I haven't found any resources that could give an example of this scenario.
struct PartnerDetailView: View {
#Binding var partner: Partner
var body: some View {
Form {
Section("Locations") {
List($partner.locations, id: \.self) { $location in
NavigationLink {
LocationDetailView(partner: $partner, locationIndex: partner.locations.firstIndex(of: location) ?? 0)
} label: {
Text(location.name)
}
}
}
}
}
}
struct LocationDetailView: View {
#Binding var partner: Partner
var locationIndex: Int
var body: some View {
TextField("Name", text: $partner.locations[locationIndex].name)
}
}
Is it possible that I am not passing values between views properly?🤔

Supply any string value to swiftUI form in different for textfield

I have two view file. I have textfield. I want to supply string value to the textfield from another view
File 1 :- Place where form is created
struct ContentView: View {
#State var subjectLine: String = ""
var body: some View {
form {
Section(header: Text(NSLocalizedString("subjectLine", comment: ""))) {
TextField("SubjectLine", text: $subjectLine
}
}
}
}
File 2 :- Place where I want to provide value to the string and that will show in the textfield UI
struct CalenderView: View {
#Binding var subjectLine: String
var body : some View {
Button(action: {
subjectLine = "Assigned default value"
}, label: {
Text("Fill textfield")
}
}
})
}
}
This is not working. Any other way we can supply value to the textfield in other view file.
As i can understand you have binding in CalenderView
that means you want to navigate there when you navigate update there.
struct ContentView: View {
#State private var subjectLine: String = ""
#State private var showingSheet: Bool = false
var body: some View {
NavigationView {
Form {
Section(header: Text(NSLocalizedString("subjectLine", comment: ""))) {
TextField("SubjectLine", text: $subjectLine)
}
}
.navigationBarItems(trailing: nextButton)
.sheet(isPresented: $showingSheet) {
CalenderView(subjectLine: $subjectLine)
}
}
}
var nextButton: some View {
Button("Next") {
showingSheet.toggle()
}
}
}
CalendarView
struct CalenderView: View {
#Binding var subjectLine: String
#Environment(\.dismiss) private var dismiss
var body: some View {
Button {
subjectLine = "Assigned default value"
dismiss()
} label: {
Text("Fill textfield")
}
}
}

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

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() })
}
}
}

Resources