Use an object to collect form data and set default value in Swift - 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...
}
}

Related

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.

SwiftUI ForEach Binding compile time error looks like not for-each

I'm starting with SwiftUI and following WWDC videos I'm starting with #State and #Binding between two views. I got a display right, but don't get how to make back-forth read-write what was not include in WWDC videos.
I have model classes:
class Manufacturer {
let name: String
var models: [Model] = []
init(name: String, models: [Model]) {
self.name = name
self.models = models
}
}
class Model: Identifiable {
var name: String = ""
init(name: String) {
self.name = name
}
}
Then I have a drawing code to display that work as expected:
var body: some View {
VStack {
ForEach(manufacturer.models) { model in
Text(model.name).padding()
}
}.padding()
}
and I see this:
Canvas preview picture
But now I want to modify my code to allows editing this models displayed and save it to my model #Binding so I've change view to:
var body: some View {
VStack {
ForEach(self.$manufacturer.models) { item in
Text(item.name)
}
}.padding()
}
But getting and error in ForEach line:
Generic parameter 'ID' could not be inferred
What ID parameter? I'm clueless here... I thought Identifiable acting as identifier here.
My question is then:
I have one view (ContentView) that "holds" my datasource as #State variable. Then I'm passing this as #Binding to my ManufacturerView want to edit this in List with ForEach fill but cannot get for each binding working - how can I do that?
First, I'm assuming you have something like:
#ObservedObject var manufacturer: Manufacturer
otherwise you wouldn't have self.$manufacturer to begin with (which also requires Manufacturer to conform to ObservableObject).
self.$manufacturer.models is a type of Binding<[Model]>, and as such it's not a RandomAccessCollection, like self.manufacturer.models, which is one of the overloads that ForEach.init accepts.
And if you use ForEach(self.manufacturer.models) { item in ... }, then item isn't going to be a binding, which is what you'd need for, say, a TextField.
A way around that is to iterate over indices, and then bind to $manufacturer.models[index].name:
ForEach(manufacturer.indices) { index in
TextField("model name", self.$manufacturer.models[index].name)
}
In addition to that, I'd suggest you make Model (and possibly even Manufacturer) a value-type, since it appears to be just a storage of data:
struct Model: Identifiable {
var id: UUID = .init()
var name: String = ""
}
This isn't going to help with this problem, but it will eliminate possible issues with values not updating, since SwiftUI wouldn't detect a change.

SwiftUI bind array of objects and show changes

I'm trying to build a little app in SwiftUI. It is supposed to show a list of items an maybe change those. However, I am not able to figure out, how the data flow works correctly, so that changes will be reflected in my list.
Let's say I have a class of Item like this:
class Item: Identifiable {
let id = UUID()
var name: String
var dateCreated: Date
}
And this class has an initializer, that assigns each member a useful random value.
Now let's say I want to store a list of items in another class like this:
class ItemStore {
var items = [Item]()
}
This item store is part of my SceneDelegate and is handed to the ContextView.
Now what I want to do is hand one element to another view (from the stack of a NavigationView), where it will be changed, but I don't know how to save the changes made so that they will be reflected in the list, that is shown in the ContextView.
My idea is to make the item store an environment object. But what do I have to do within the item class and how do I have to pass the item to the other view, so that this works?
I already tried something with the videos from Apple's WWDC, but the wrappers there are deprecated, so that didn't work.
Any help is appreciated. Thanks a lot!
The possible approach is to use ObservableObject (from Combine) for storage
class ItemStore: ObservableObject {
#Published var items = [Item]()
// ... other code
}
class Item: ObservableObject, Identifiable {
let id = UUID()
#Published var name: String
#Published var dateCreated: Date
// ... other code
}
and in dependent views
struct ItemStoreView: View {
#ObservedObject var store: ItemStore
// ... other code
}
struct ItemView: View {
#ObservedObject var item: Item
// ... other code
}

SwiftUI Picker with selection as struct

Iam trying to use Picker as selection of struct. Let say I have a struct "Pet" like below
struct Pet: Identifiable, Codable, Hashable {
let id = UUID()
let name: String
let age: Int
}
I am getting all Pet's from some class, where Pets are defined as #Published var pets = Pet
static let pets = Class().pets
I would like to be able to write a selection from picker to below variable:
#State private var petSelection: Pet?
Picker is:
Picker("Pet", selection: $petSelection){
ForEach(Self.pets) { item in
Text(item.name)
}
}
Picker shows properly all avaliavble pets but when I chose one petSelection has been not changed (nil). How should I mange it?
Thanks!
Edit:
Of course I know that I can use tag like below:
Picker("Pet", selection: $petSelection) {
ForEach(0 ..< Self.pet.count) { index in
Text(Self.pet[index].name).tag(index)
}
But wonder is it possible to use struct as selection. Thanks
Short answer: The type associated with the tag of the entries in your Picker (the Texts) must be identical to the type used for storing the selection.
In your example: You have an optional selection (probably to allow "empty selection") of Pet?, but the array passed to ForEach is of type [Pet]. You have to add therefore a .tag(item as Pet?) to your entries to ensure the selection works.
ForEach(Self.pets) { item in
Text(item.name).tag(item as Pet?)
}
Here follows my initial, alternate answer (getting rid of the optionality):
You have defined your selection as an Optional of your struct: Pet?. It seems that the Picker cannot handle Optional structs properly as its selection type.
As soon as you get rid of the optional for example by introducing a "dummy/none-selected Pet", Picker starts working again:
extension Pet {
static let emptySelection = Pet(name: "", age: -1)
}
in your view initialise the selection:
#State private var petSelection: Pet = .emptySelection
I hope this helps you too.
You use the following way:
#Published var pets: [Pet?] = [ nil, Pet(name: "123", age: 23), Pet(name: "123dd", age: 243),]
VStack{
Text(petSelection?.name ?? "name")
Picker("Pet", selection: $petSelection){
ForEach(Self.pets, id: \.self) { item in
Text(item?.name ?? "name").tag(item)
}}}
the type of $petSelection in Picker(selection:[...] has to be the same type of id within your struct.
So in your case you would have to change $petSelection to type if UUID since your items within the collection have UUID as identifier.
Anyway since this is not what you're after, but your intention is to receive the Pet as a whole when selected. For that you will need a wrapper containing Pet as the id. Since Pet is already Identifiable, there're only a few adjustments to do:
Create a wrapper having Pet as an id
struct PetPickerItem {
let pet: Pet?
}
Now wrap all collection items within the picker item
Picker("Pet", selection: $petSelection) {
ForEach(Self.pets.map(PetPickerItem.init), id: \.pet) {
Text("\($0.pet?.name ?? "None")")
}
}
You can now do minor adjustments like making PetPickerItem identifiable to remove the parameter id: from ForEach.
That's the best solution I came up with.
This is how I do it:
struct Language: Identifiable, Hashable {
var title: String
var id: String
}
struct PickerView: View {
var languages: [Language] = [Language(title: "English", id: "en-US"), Language(title: "German", id: "de-DE"), Language(title: "Korean", id: "ko-KR")]
#State private var selectedLanguage = Language(title: "German", id: "de-DE")
var body: some View {
Picker(selection: $selectedLanguage, label: Text("Front Description")) {
ForEach(self.languages, id: \.self) {language in
Text(language.title)
}
}
}