How to update global variable with TextField? - swift

I have some variables that I need in several views and I want the user to be able to change these. These variables edit a link, so depending on for example what the longitude is the result is different. And the values I get from the link are displayed in a graph.
So I want the user to be able to enter their own value for the given variables(in the code below). I don't care if it is in the same view, or if consists of two views and the variables are passed to the view with the graphs.
The only important thing, is that the graph and the link refreshes when the user clicks a button, so that the right graph is displayed.
Another important thing, is that I need to be able to access the variables at top-level. This is my code until now, but it shows the error:
Cannot convert value of type 'string' to expected argument type 'Binding'
import SwiftUI
var month = "1"
var day = "1"
var year = "2020"
var latitude = "59.911232"
var longitude = "10.757933"
var offset = "1.0"
struct ContentView: View {
var body: some View {
GraphView()
TextField(text: latitude)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

TextField requires binding to some dynamic property source of truth. If you want to keep those values globally then wrap them into some shared instance of view model, like
final class Params: ObservableObject {
static let global = Params()
var month = "1"
var day = "1"
var year = "2020"
var latitude = "59.911232"
var longitude = "10.757933"
var offset = "1.0"
}
so now we are able to observe (and update, and bind to) those parameters from view
struct ContentView: View {
#ObservedObject var global = Params.global
var body: some View {
GraphView()
TextField(text: $global.latitude)
}
}

Related

Use an object to collect form data and set default value in Swift

I'm brand new in Swift development, and I can't for the life of me figure this out. All I want to do is use an object to collect a forms data, save it to Realm, and then send it to the server via API.
In every example I've found, people are creating a new State variable for each element in their form. This seems unpractical for forms with many fields, so I tried to just create an object with properties that match the form fields I need. If I don't try to set any default values, this works as I expect. But when I try to set some default values for the form in the init(), I get errors that I don't know how to resolve. Here's some partial code:
The object that will be used to collect the form data:
class RecordObject: ObservableObject {
var routeId: Int?
var typeId: Int?
var inDate: Date?
var outDate: Date?
var nextDate: Date?
// .... more properties
}
And what I want to do is in the View, set some default values in the init() that need some logic to derive the value:
In the View:
struct AddLauncherView: View {
var route: Route // this is passed from a previous view that lists all the routes
#StateObject var record: RecordObject = RecordObject() // this is where I want to store all the form data
init(){
_record.routeId = self.route.id // I get the error: Referencing property 'routeId' requires wrapped value of type 'RecordObject'
self.record.inDate = Date() // this gives the error: 'self' used before all stored properties are initialized
//self.record.nextDate = here I need to do some date math to figure out the next date
}
var body: some View {
Form{
DatePicker("In Date", selection: $record.inDate, displayedComponents: .date)
// .... more form elements
}
}
}
I know I could add default values in the RecordObject class, but there are some properties that will need some logic to assign the default value.
Can someone help out a Swift noob and give me some pointers for making this work? Do I really need to create a State var for each form field in the View?
If you did use a class (ObservableObject), you'd want the properties to be annotated with #Published. However, it's probably a better idea to use a struct with a #State variable that contains all of the various properties you need. That may look like this:
struct Record {
var routeId: Int?
var typeId: Int?
var inDate: Date?
var outDate: Date?
var nextDate: Date?
}
struct AddLauncherView: View {
var route: Route
#State var record: Record
init(route: Route) {
self.route = route
_record = State(initialValue: Record(routeId: route.id,
typeId: nil,
inDate: Date(),
outDate: nil,
nextDate: nil))
}
var body: some View {
Form{
DatePicker("In Date",
selection: Binding<Date>(get: {record.inDate ?? Date()},
//custom binding only necessary if inDate is Date? -- if you make it just Date, you can bind directly to $record.inDate
set: {record.inDate = $0}),
displayedComponents: .date)
// .... more form elements
}
}
}
You are using the ObservableObject incorrectly. You data model should be a struct, and then the class publishes values that are of the type of the struct, so:
struct Record: Identifiable {
// First, save yourself some grief later and make it conform to Identifiable
let id = UUID()
// The listed variables are probably non-optional in your data model.
// If they are optional, mark them as optional, otherwise
var routeId: Int
var typeId: Int
var inDate: Date
// These are more likely optional
var outDate: Date?
var nextDate: Date?
// .... more properties
}
class RecordObject: ObservableObject {
// Make the source of truth #Published so things are updated in your view
#Published var records: [Record] = []
// OR use an init
#Published var records: [Record]
init(records: [Records] {
self.records = records
// or call some function that imports/creates the records...
}
}

Swift & SwiftUI - Conditional global var

I want to make a global variable in Swift, so that its Data is accessible to any view that needs it. Eventually it will be a var so that I can mutate it, but while trying to get past this hurdle I'm just using it as let
I can do that by putting this as the top of a file (seemingly any file, Swift is weird):
let myData: [MyStruct] = load("myDataFile.json)
load() returns a JSONDecoder(). MyStruct is a :Hashable, Codable, Identifiable struct
That data is then available to any view that wants it, which is great. However, I want to be able to specify the file that is loaded based on a condition - I'm open to suggestions, but I've been using an #AppStorage variable to determine things when inside a View.
What I'd like to do, but can't, is do something like:
#AppStorage("appStorageVar") var appStorageVar: String = "Condition1"
if(appStorageVar == "Condition2") {
let myData: [MyStruct] = load("myDataFile2.json")
}
else {
let myData: [MyStruct] = load("myDataFile.json")
}
I can do this inside a View's body, but then it's only accessible to that View and then I have to repeat it constantly, which can't possibly the correct way to do it.
You could change just change the global in an onChange on the AppStorage variable. This is an answer to your question, but you have the problem that no view is going to be updating itself when the global changes.
var myData: [MyStruct] = load("myDataFile.json)
struct ContentView: View {
#AppStorage("appStorageVar") var appStorageVar: String = "Condition1"
var body: some View {
Button("Change value") {
appStorageVar = "Condition2"
}
.onChange(of: appStorageVar) { newValue in
myData = load(newValue == "Condition1" ? "myDataFile.json" : "myDataFile2.json")
}
}
}

How to delay setting value of #State variable in SwiftUI

I want to create an application with a date picker. The initial value of the date picker (startDate) requires some computation performed in another class. (Let's say for example that this date is initially set to a customer's enrolment date in a service, which is retrieved from a database.) The user can leave that date as-is, or select a new date using the date picker.
I haven't been able to find a way of setting this initial value. The following simplified code illustrates the issue. The date picker shows the initial value of today's date rather than the desired date (here, Date.distantPast.) The date picker otherwise works fine.
Things I've tried:
If I set the value in the init clause (as shown below), it is ignored (no error reported, but the value is not written to startDate variable).
Trying to set the value of startDate inline with its declaration gives a compile-time error message ("Cannot use instance member 'db' within property initializer; property initializers run before 'self' is available".)
Wrapping the DataPicker in an if-statement so it won't not be displayed until the startDate has been computed makes no difference.
Is there any way to defer the setting of the initial date selection in a date picker until it can be computed?
import SwiftUI
struct ContentView: View {
private let db = DB()
#State private var startDate = Date()
var body: some View {
DatePicker("Start Date",
selection: $startDate,
displayedComponents: [.date])
}
init() {
startDate = db.theDate
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class DB {
var theDate: Date
init() {
theDate = Date.distantPast
}
}
You can create a ViewModel to match your view. In the ViewModel you have more possibilities. For example, you can easily initialise date in the Init.
This would look something like this:
class ContentViewModel: ObservableObject {
private let db = DB()
#Published var date: Date
init() {
date = db.theDate
}
}
struct ContentView: View {
#ObservedObject var viewModel = ContentViewModel()
var body: some View {
DatePicker("Start Date",
selection: $viewModel.date,
displayedComponents: [.date])
}
}
Make sure that the ViewModel is an ObservableObject and that properties that can change use the property wrapper #Published.
If your app no longer needs to support iOS 13, you can also use #StateObject instead of #ObservedObject.

Cannot convert value of type 'Published<[StepsEntity]>.Publisher' to expected argument type 'Binding<String>'

StepsEntity is a core data entity
Receiving the following error when attempting to display a string value in a TextField: "Cannot convert value of type 'Published<[StepsEntity]>.Publisher' to expected argument type 'Binding'"
I know this is because StepsEntity in my core data model is #Published. #Published works great here as it allows all the data to be updated neatly. How can I display an #Published in a TextField?
Below is the piece where I am receiving the error:
List {
VStack(alignment: .center) {
if let recipeSteps = (vm.recipes[vm.getRecordsCount() - 1].steps?.allObjects as? [StepsEntity])?.sorted { $0.stepNumber < $1.stepNumber } {
if (textFieldCount == 1) {
//do nothing
} else if (textFieldCount > 1) {
ForEach(recipeSteps, id: \.stepNumber) { index in
HStack {
Text(String(index.stepNumber) + ".").bold()
TextField("", text: vm.$recipeSteps) //This is where the error is seen
}
}.onDelete(perform: { index in
self.vm.deleteRecipeSteps(at: index, from: vm.recipes[vm.getRecordsCount() - 1])
})
}
}
}
vm.recipeSteps refers to my CoreDataRelationshipViewModel, which is where all core data functions are handled. this is declared in the view struct as:
#StateObject var vm = CoreDataRelationshipViewModel()
Here is a snippet from the CoreDataRelationshipViewModel class:
class CoreDataRelationshipViewModel: ObservableObject {
let manager = CoreDataManager.instance
#Published var recipes: [RecipeEntity] = []
#Published var recipeSteps: [StepsEntity] = []
init() {
getRecipes()
}
func getRecipes() { ////more functions for retrieving, deleting, etc. in this section
I have tried converting the Published var to a binding but no luck:
TextField("", text: Binding(vm.$recipeSteps)!)
I have also tried referencing the recipeSteps declared in the if let statement within the list, but that does not work either.
I have been at it for a few days, and I think I have exhausted all options. Open to all ideas here. Maybe I need to rebuild my model?
Thoughts?
--Edits--
Upper portion of struct, where variables are created:
struct AddItemView: View {
#StateObject var viewModel = ViewModel()
#State var frameDimensions: CGFloat = 0
#State var imageButtonText: String = "Click To Add Image"
#State var imageToUpload: Data
#StateObject var vm = CoreDataRelationshipViewModel()
#Environment(\.presentationMode) var presentationMode
#State var stepInfo: String = ""
#State var textFieldCount: Int = 1
#State var stepNumber: [Int]
#State var recordsCount = 1
#State var errorText = ""
#State var stepErrorColor = Color.white.opacity(0)
#State var nameErrorColor = Color.white.opacity(0)
var body: some View {
ZStack {
HStack {
Your problem is that
TextField("", text: vm.$recipeSteps)
should actually be
TextField("", text: $vm.recipeSteps)
as you need to pass the view model with Binding rather than recipeSteps (which, when using $ to pass it beyond vm's dot accessor, passes a generic Publisher).
Also, this would only work if the #Published property in your view model conforms to StringProtocol (is a string). Are you sure recipeSteps is a string that can be edited via TextField?
I ended up restructuring the way my core data save works. Instead of saving item by item, I am now looping through a for in loop to save data all at the end. This allows me to display the data locally via some state variables, but then save the full array to my core data entity.

Learning to data between parent and child views in swiftui

I have my parent view where I'm setting my States and I have two different views I'm trying to pass data between. When subview_1 calls subview_2 I'm not getting the binding data, the compiler fails with the error, The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions.
My parent view I've set the states
#State var businessPhone = ""
#State var businessRating = 0.0
In subview_1 I have my bindings set and the data works fine.
struct subview_1: View {
#Binding var businessRating: Double
#Binding var businessPhone: String
var body: some View {
....
}
}
When I call subview_2 from subview_1 the data isn't getting passed as expected.
struct subview_1: View {
#Binding var businessRating: Double
#Binding var businessPhone: String
var body: some View {
subview_2(businessRating: $businessRating, businessPhone: $businessPhone)
}
}
If I use .constant() with data that works as expected.
For subview_2 I have the following bindings set
#Binding var businessRating: Double
#Binding var businessPhone: String