SwiftUI pass a computed variable to next view - swiftui

I'm new to IOS and SwiftUI coding. I googled a lot but could not find a solution, how to pass a computed variable to the next view.
Here snippets of what I have:
import SwiftUI
struct ContentView: View {
#State private var isShowingResultView = false
#State private var netRate = "0"
#State var daysMonth = "0"
#State var hoursWeek: String = "0"
#State var daysOnsite: String = "0"
#State var ticketCost: String = "0"
#State var hotelCost: String = "0"
#State var otherCost: String = "0"
//#State var travellCostResult: Double = 0.00
var travellCostPerHour: Double{
get {
let daysMonthNbr = Int(daysMonth) ?? 0
let hoursWeekNbr = Int(hoursWeek) ?? 0
let daysOnsiteNbr = Int(daysOnsite) ?? 0
let ticketCostNbr = Double(ticketCost) ?? 0
let hotelCostNbr = Double(hotelCost) ?? 0
let otherCostNbr = Double(otherCost) ?? 0
let travellCostPerWeek = (ticketCostNbr + (Double((daysOnsiteNbr-1))*hotelCostNbr)+otherCostNbr)
let travellCostPerHour: Double = Double(travellCostPerWeek) / Double(hoursWeekNbr)
return travellCostPerHour.isNaN ? 0 : travellCostPerHour
}
}
.
.
.
var body: some View {
HStack {
NavigationLink("Calculate", destination: ResultView(netRate: self.$netRate, travellCostPerHour: travellCostPerHour), isActive: $isShowingResultView).navigationBarTitle("Result").buttonStyle(GradientButtonStyle())
.adaptToKeyboard()
}
struct ResultView: View {
#Binding var netRate: String
#Binding var travellCostPerHour: Double
.
.
.
struct ResultView_Previews: PreviewProvider {
#State static var netRate: String = ""
#State static var travellCostPerHour: Double = 0.00
static var previews: some View {
ResultView(netRate: $netRate, travellCostPerHour: $travellCostPerHour )
}
}
I get this error msg in the navigationLink for travellCostPerHour: Cannot convert value of type 'Double' to expected argument type 'Binding'
Can one put me on the right path here please?

If it is computed property then binding is not needed, pass it as-is
struct ResultView: View {
#Binding var netRate: String
var travellCostPerHour: Double // << just regular
// .. other code

Related

Updating slider value with Swift UI

I'm trying to pass data from one view to another and update the slider.
I understand you can do this with #State and #Binding property wrappers however it does not appear to be updating.
On my content view I have:
#State var startNumber: Double = 0
#State var endNumber: Double = 5000
Slider(value: $startNumber, in: 0...10000, step: 0.1)
On my second view I have:
#Binding var startNumber: Double
#Binding var endNumber: Double
#State var number: String = ""
#State var number2: String = ""
FirstResponderTextField(text: $number2, placeholder: "\(endNumber)")
Button(action: {
startNumber = Double(number) ?? 0
endNumber = Double(number2) ?? 0
print("\(startNumber)")
print("\(number)")
print("\(endNumber)")
print("\(number2)")
self.presentationMode.wrappedValue.dismiss()
}
I can see that the value doesn't update the #Binding as it stays at 0.
So I can't even get the variable to update, would someone know why?
Then I understand for the value to show on the content view that I would need to call onAppear after the Hstack for it to show the updated value?
HStack {
}onAppear {
self.startNumber = $startNumber
}
However, this doesn't seem to work either?

SwiftUi: Navigating to a secondary page through Button Alert Confirmation

I'm trying to figure out how to navigate to my secondary screen through a button alert. Firstly, the user inputs some fields through textfields in the primary screen.
At the bottom of this screen the user presses a submit button which then pops up an alert asking whether they would like to be taken to the secondary screen or cancel to not be taken. All the fields that have been entered through the primary screen then get passed onto the secondary screen. The user than has the option to navigate back to the primary screen if necessary once in the secondary screen.
Here's what I've tried:
struct View1: View {
#State var txtField1 : String = ""
#State var txtField2: String = ""
#State var txtField3: String = ""
#State var txtField4: String = ""
#State var txtField5 : String = ""
#State private var showingAlert = false
#State private var showingView = false
var body: some View {
HStack{
Button(action: {
self.showingAlert = true
}) {
Text("Submit")
.alert(isPresented:$showingAlert) {
Alert(title: Text("Would you like to go to second screen?"), message: Text("The second screen will pass all data from the first screen."), primaryButton:.destructive(Text("Continue")){
self.showingView = true
}, secondaryButton: .cancel(Text("Cancel")))
}
}
}.popover(isPresented: $showingView){
NavigationView{
View2(txtField1: self.$txtField1, txtField2: self.$txtField2, txtField3: self.$txtField4, txtField5: self.$txtField5)
}
}
When using the code above it does navigate to my secondary screen (View2) however its like a sheet. View2 has no navigation properties back to View1 and this is what I'm trying to achieve. Any help on this matter is greatly appreciated, Thank you!
To get the Back button you need a NavigationLink vs a popover. You can just "hide" the NavigationLink next to your Button
import SwiftUI
struct ConfirmNavView: View {
#State var txtField1 : String = ""
#State var txtField2: String = ""
#State var txtField3: String = ""
#State var txtField4: String = ""
#State var txtField5 : String = ""
#State private var showingAlert = false
#State private var showingView = false
var body: some View {
NavigationView{
HStack{
Button(action: {
self.showingAlert = true
}) {
Text("Submit")
.alert(isPresented:$showingAlert) {
Alert(title: Text("Would you like to go to second screen?"), message: Text("The second screen will pass all data from the first screen."), primaryButton:.destructive(Text("Continue")){
self.showingView = true
}, secondaryButton: .cancel(Text("Cancel")))
}
}
NavigationLink("View2", destination: View2(txtField1: self.$txtField1,
txtField2: self.$txtField2,
txtField3: self.$txtField3,
txtField4: self.$txtField4,
txtField5: self.$txtField5), isActive: $showingView).hidden().frame(width: 0, height: 0)
}
}
}
}
struct View2: View {
#Binding var txtField1 : String
#Binding var txtField2: String
#Binding var txtField3: String
#Binding var txtField4: String
#Binding var txtField5 : String
var body: some View {
VStack{
TextField("txtField1", text: $txtField1)
TextField("txtField2", text:$txtField2)
TextField("txtField3", text:$txtField3)
TextField("txtField4", text:$txtField4)
TextField("txtField5", text:$txtField5)
}
}
}
struct ConfirmNavView_Previews: PreviewProvider {
static var previews: some View {
ConfirmNavView()
}
}

Why does SwiftUI update function doesn't work?

There are 2 views (structs).
First view has a #state update:
struct SettingsView: View {
#State private var lang = 0
#State private var languages = ["English", "Spanish"]
#State private var text1 = "Close"
#State private var text2 = "Settings"
#State var show = false
#State var update = false
var body: some View {
ZStack{
Button(action: {
self.show.toggle()
}) {
Text("Choose language")
}
if self.$show.wrappedValue {
GeometryReader {proxy in
ChooseLanguage(show: self.$show, update: self.$update)
}.background(Color.black.opacity(0.65)
.edgesIgnoringSafeArea(.all)
.onTapGesture {
withAnimation{
self.show.toggle()
}
})
}
}.onAppear{
switch UserDefaults.standard.string(forKey: "languageSettings"){
case "en": self.lang = 0
case "es": self.lang = 1
default: return
}
self.updateLanguage()
}
func updateLanguage(){
if self.lang == 1 {
self.text1 = "Cerrar"
self.text2 = "Configuración"
self.languages = ["Inglés", "Español"]
} else {
self.text1 = "Close"
self.text2 = "Settings"
self.languages = ["English", "Spanish"]
}
}
}
}
The second view has #Binding update:
import SwiftUI
struct ChooseLanguage : View {
var languages = UserDefaults.standard.stringArray(forKey: "langlist")
#Binding var show: Bool
#Binding var update: Bool
var body: some View {
ZStack {
VStack {
Button(action: {
UserDefaults.standard.set("en", forKey: "languageSettings")
UserDefaults.standard.set(["English", "Spanish"], forKey: "langlist")
self.show.toggle()
self.update = true
}) {
Text(languages![0])
}
Button(action: {
UserDefaults.standard.set("es", forKey: "languageSettings")
UserDefaults.standard.set(["Inglés", "Español"], forKey: "langlist")
self.show.toggle()
self.update = true
}) {
Text(languages![1])
}
}
}
}
}
When I call the func updateLanguage() before the .onAppear only errors appear.
Why I can update the values with function from the onAppear and I can't do this from the wrappedValue?
if self.$update.wrappedValue {
self.updateLanguage()
self.update.toggle()
}
This part doesn't work if to place before }.onAppear
As far as I see, you can make it so much easier with using init() method for your view.
There you can declare and initialize all your #State variables with the correct value (depending on your UserDefaults)
Just to show you an example:
struct SetView: View {
#State private var lang : Int
#State private var languages : [String]
#State private var text1 : String
#State private var text2 : String
#State var show = false
#State var update = false
init()
{
var state : Int = 0
switch UserDefaults.standard.string(forKey: "languageSettings")
{
case "en": state = 0
case "es": state = 1
default:
//Default value here
state = 0
}
if state == 1 {
self._lang = State(initialValue: state)
self._text1 = State(initialValue: "Cerrar")
self._text2 = State(initialValue: "Configuración")
self._languages = State(initialValue: ["Inglés", "Español"])
} else {
self._lang = State(initialValue: state)
self._text1 = State(initialValue: "Close")
self._text2 = State(initialValue: "Settings")
self._languages = State(initialValue: ["English", "Spanish"])
}
}
You won't need onAppear method at all, when you initialize your State variables with the correct value from the beginning.
I haven't tested it yet. Code is just out of my mind above.
Instead of calling the function after the #State upload value has been changed easier to send bindings for each text to the popup view.
GeometryReader {proxy in
ChooseLanguage(show: self.$show,
text1: self.$text1,
text2: self.$text2,
languages: self.$languages)
}

How to pass State variables as parameters to a model class which is of ObservableObject type?

I want to save some data from a SwiftUI view to a Model so that I can use these data into another SwiftUI view. However, I came up with some error when I try to call the Model class and pass all the data as parameters. The error says:
Cannot use instance member 'expectedEndDate' within property initializer; property initializers run before 'self' is available"
Here is my SearchBikeDataModel() code:
import Foundation
class SearchBikeDataModel: ObservableObject {
#Published var startDate: Date = Date()
#Published var startTime: Date = Date()
#Published var endDate: Date = Date()
#Published var endTime: Date = Date()
#Published var bikeType: String = ""
#Published var collectionPoint: String = ""
#Published var returnPoint: String = ""
init(selectedStartDate: Date, selectedStartTime: Date, selectedEndDate: Date, selectedEndTime: Date, bikeType: String, collectionPoint: String, returnPoint: String) {
self.startDate = selectedStartDate
self.startTime = selectedStartTime
self.endDate = selectedEndDate
self.endTime = selectedEndTime
self.bikeType = bikeType
self.collectionPoint = collectionPoint
self.returnPoint = returnPoint
}
}
And here is the code where I try to pass data as parameters:
import SwiftUI
struct BikeSearchFormView: View {
#Binding var isDateTimeShown: Bool
#Binding var isEndDateTimePickerShown: Bool
#State var expectedStartDate = Date()
#State var expectedStartTime = Date()
#State var expectedEndDate = Date()
#State var expectedEndTime = Date()
#State var isBikeTypePickerExpand: Bool = false
#State var isDropOffPointPickerExpand: Bool = false
#State var isPickUpPointPickerExpand: Bool = false
#State var selectedBikeType: String = "BIKE TYPE"
#State var selectedDropOffPoint: String = "DROP OFF POINT"
#State var selectedPickUpPoint: String = "PICKUP POINT"
#State var findBikeError: String = ""
#State var isActive: Bool = false
#ObservedObject var bikeTypeViewModel = VehicleTypeViewModel()
#ObservedObject var findBikeViewModel = FindBikeViewModel()
#ObservedObject var dataModel = SearchBikeDataModel(selectedStartDate: expectedStartDate, selectedStartTime: expectedStartTime, selectedEndDate: expectedEndDate, selectedEndTime: expectedEndTime, bikeType: selectedBikeType, collectionPoint: selectedPickUpPoint, returnPoint: selectedDropOffPoint)
var body: some View {
Text("Hello, World")
}
}
I have omitted codes of my UI as the question is just about am I following the right way to pass the data into parameters.

SwiftUI #State var initialization issue

I would like to initialise the value of a #State var in SwiftUI through the init() method of a Struct, so it can take the proper text from a prepared dictionary for manipulation purposes in a TextField.
The source code looks like this:
struct StateFromOutside: View {
let list = [
"a": "Letter A",
"b": "Letter B",
// ...
]
#State var fullText: String = ""
init(letter: String) {
self.fullText = list[letter]!
}
var body: some View {
TextField($fullText)
}
}
Unfortunately the execution fails with the error Thread 1: Fatal error: Accessing State<String> outside View.body
How can I resolve the situation? Thank you very much in advance!
SwiftUI doesn't allow you to change #State in the initializer but you can initialize it.
Remove the default value and use _fullText to set #State directly instead of going through the property wrapper accessor.
#State var fullText: String // No default value of ""
init(letter: String) {
_fullText = State(initialValue: list[letter]!)
}
I would try to initialise it in onAppear.
struct StateFromOutside: View {
let list = [
"a": "Letter A",
"b": "Letter B",
// ...
]
#State var fullText: String = ""
var body: some View {
TextField($fullText)
.onAppear {
self.fullText = list[letter]!
}
}
}
Or, even better, use a model object (a BindableObject linked to your view) and do all the initialisation and business logic there. Your view will update to reflect the changes automatically.
Update: BindableObject is now called ObservableObject.
The top answer is incorrect. One should never use State(initialValue:) or State(wrappedValue:) to initialize state in a View's init. In fact, State should only be initialized inline, like so:
#State private var fullText: String = "The value"
If that's not feasible, use #Binding, #ObservedObject, a combination between #Binding and #State or even a custom DynamicProperty
In your specific case, #Bindable + #State + onAppear + onChange should do the trick.
More about this and in general how DynamicPropertys work, here.
It's not an issue nowadays to set a default value of the #State variables inside the init method. But you MUST just get rid of the default value which you gave to the state and it will work as desired:
,,,
#State var fullText: String // <- No default value here
init(letter: String) {
self.fullText = list[letter]!
}
var body: some View {
TextField("", text: $fullText)
}
}
Depending on the case, you can initialize the State in different ways:
// With default value
#State var fullText: String = "XXX"
// Not optional value and without default value
#State var fullText: String
init(x: String) {
fullText = x
}
// Optional value and without default value
#State var fullText: String
init(x: String) {
_fullText = State(initialValue: x)
}
The answer of Bogdan Farca is right for this case but we can't say this is the solution for the asked question because I found there is the issue with the Textfield in the asked question. Still we can use the init for the same code So look into the below code it shows the exact solution for asked question.
struct StateFromOutside: View {
let list = [
"a": "Letter A",
"b": "Letter B",
// ...
]
#State var fullText: String = ""
init(letter: String) {
self.fullText = list[letter]!
}
var body: some View {
VStack {
Text("\(self.fullText)")
TextField("Enter some text", text: $fullText)
}
}
}
And use this by simply calling inside your view
struct ContentView: View {
var body: some View {
StateFromOutside(letter: "a")
}
}
You can create a view model and initiate the same as well :
class LetterViewModel: ObservableObject {
var fullText: String
let listTemp = [
"a": "Letter A",
"b": "Letter B",
// ...
]
init(initialLetter: String) {
fullText = listTemp[initialLetter] ?? ""
}
}
struct LetterView: View {
#State var viewmodel: LetterViewModel
var body: some View {
TextField("Enter text", text: $viewmodel.fullText)
}
}
And then call the view like this:
struct ContentView: View {
var body: some View {
LetterView(viewmodel: LetterViewModel(initialLetter: "a"))
}
}
By this you would also not have to call the State instantiate method.
See the .id(count) in the example come below.
import SwiftUI
import MapKit
struct ContentView: View {
#State private var count = 0
var body: some View {
Button("Tap me") {
self.count += 1
print(count)
}
Spacer()
testView(count: count).id(count) // <------ THIS IS IMPORTANT. Without this "id" the initializer setting affects the testView only once and calling testView again won't change it (not desirable, of course)
}
}
struct testView: View {
var count2: Int
#State private var region: MKCoordinateRegion
init(count: Int) {
count2 = 2*count
print("in testView: \(count)")
let lon = -0.1246402 + Double(count) / 100.0
let lat = 51.50007773 + Double(count) / 100.0
let myRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: lat, longitude: lon) , span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01))
_region = State(initialValue: myRegion)
}
var body: some View {
Map(coordinateRegion: $region, interactionModes: MapInteractionModes.all)
Text("\(count2)")
}
}