How to conform a custom object to Codable - ios

I've been trying to save a UserData object so that content is "permanent". All of the methods online show classes with primitive types like String, Int, Double, etc. I am aware that you can store Data types but I couldn't find much
This is the class I want to save to disk. It errors out with : "Type 'UserData' does not conform to protocol 'Decodable'" and "Type 'UserData' does not conform to protocol 'Encodable'"
Obviously, skillBag is causing the issue; but I can't find any resources on how to conform Skill to Codable.
class UserData : ObservableObject, Codable{
private var userName:String;
#Published private var skillBag:[Skill] = [];
init(Username user:String, skillBag skills:[Skill]) {
userName = user;
skillBag = skills;
}
}
Skill Class
class Skill : Identifiable, ObservableObject, Equatable{
#Published var skillName:String = "";
#Published var level:Int = 1;
#Published var prestiege:Int = 0;
var curXP:Double = 0.0;
var maxXP:Double = 50.0;
#Published var skillDesc:String;
#Published public var skillColor:Color = Color.purple;
#Published public var buttons: [SkillButton] = [];
#Published private var prestCap:Int;
//EXTRA HIDDEN CODE
}
For Reference, here is the SkillButton Class
class SkillButton : Identifiable, ObservableObject, Equatable{
#Published public var xp:Double = 0;
#Published private var buttonName:String = "";
#Published private var uses:Int = 0;
#Published public var buttonColor:Color = Color.pink;
#Published private var description:String;
enum CodingKeys: String, CodingKey {
case buttonName, xp, uses, buttonColor, description
}
init(ButtonName buttonName:String, XpGained xp:Double, Uses uses:Int = 0, Description desc:String = "") {
self.xp = xp;
self.buttonName = buttonName;
self.uses = uses;
self.description = desc;
}
}
I know that each of these classes need to conform to Codable, but I just couldn't find a resource to make these conform to Codable.

Other than not conforming the Skill and SkillButton classes to Codable, the underlying problem you have is down to the #published property wrapper, as this stops the auto-synthesis of Codable conformance.
Rather than writing out the solution, can I refer you to this Hacking with Swift link: https://www.hackingwithswift.com/books/ios-swiftui/adding-codable-conformance-for-published-properties
You will also have to conform Color to Codable yourself as (unless it's changed in recent updates?) it isn't Codable out of the box.

Related

How to share an EnvironmentObject from a View to its ViewModel?

I'm using a DataStore environmentObject to pass data around my app, however I'm at a point where I needed to create a specific View Model class for a view, but I need to be able to pass the instance of my dataStore to my WorkoutDetailViewModel but can't figure out a way to do it. I tried passing it in as a parameter to WorkoutDetailViewModel, but the compiler complains Cannot use instance member 'dataStore' within property initializer; property initializers run before 'self' is available. How else can I do this?
struct NewWorkoutDetailView: View {
#EnvironmentObject var dataStore: DataStore
#StateObject var workoutDetailViewModel = WorkoutDetailViewModel(dataStore: dataStore)
var body: some View {
}
}
final class WorkoutDetailViewModel: ObservableObject {
var dataStore: DataStore
#Published var fullyLoadedWorkout: TrackerWorkout?
#Published var notes: String = ""
#Published var workoutImage = UIImage()
#Published var workoutLocation: String = ""
public func editWorkout() {
dataStore.loadWorkouts()
}
}

Method with No observable object

I have a class method that apparently doesn't see the #EnvironmentObject var categories: Categories object at the top of the class. I know that this works since I use it in several other files. It also means that my coding in SceneDelegate is correct. The software is crashing with the error: Thread 1: Fatal error: No ObservableObject of type Categories found. A View.environmentObject(_:) for Categories may be missing as an ancestor of this view. The error is occurring in the method updateTotals() in the "for" loop
struct CatItem: Codable, Identifiable {
var id = UUID()
var catNum: Int
var catName: String <-- this is the class I'm trying to reference
var catTotal: Double
var catPix: String
var catShow: Bool
}
class Categories: ObservableObject {
#Published var catItem: [CatItem]
}
class BaseCurrency: ObservableObject {
#EnvironmentObject var userData: UserData
#EnvironmentObject var categories: Categories
var foundNew: Bool = false
var newRate: Double = 0.0
var baseCur: BaseModel
//-----------------------------------------------------
// new base currency so need to update the system totals
//-----------------------------------------------------
func updateTotals() -> () {
for index in 0...(categories.catItem.count - 1) { <-- error here
categories.catItem[index].catTotal *= self.newRate
}
userData.totalArray[grandTotal] *= self.newRate
userData.totalArray[transTotal] *= self.newRate
userData.totalArray[userTotal] *= self.newRate
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(self.baseCur) {
UserDefaults.standard.set(encoded, forKey: "base")
}
self.foundNew = false
}
}
I was reading somewhere recently that #EnvironmentObject is just like #State in that a change in either parameter will cause body to update the view. Therefore neither of these should be in a class. I have reorganized my software and have not seen the error since.

Problems with class initialization

I think it's a simple problem but a head scratcher.
I'm getting a ....has no initializers error when there actually are initializers.
Here is my code:
(Edit: I've updated the PatientData class to include all the additional variables. I didn't think they made a difference in figuring out the problem, so for the sake of brevity I left them out.)
Data Structures
class PatientData: Identifiable, ObservableObject
{
let id = UUID()
#Published var patientName: String = "Name"
#Published var patientAge: String = "Age"
#Published var patientDOB: String = "DOB"
#Published var patientPhone: String = "Phone"
#Published var patientAddress: PatientAddress
struct PatientAddress
{
var patientStreetAddress: String = "Street"
var patientCity: String = "City"
var patientState: String = "State"
var patientZip: String = "Zip"
init(patientStreetAddress: String, patientCity: String, patientState: String, patientZip: String)
{
self.patientStreetAddress = patientStreetAddress
self.patientCity = patientCity
self.patientState = patientState
self.patientZip = patientZip
}
}
#Published var facilityName: String = "Facility"
#Published var facilityRoom: String = "Room Number"
#Published var facilityFloor: String = "Floor"
#Published var facilityPhoneNumber: String = "Phone Number"
init(patientName: String, patientAge: String, patientDOB: String, patientPhone: String, patientAddress: PatientAddress, facilityName: String, facilityRoom: String, facilityFloor: String, facilityPhoneNumber: String)
{
self.patientName = patientName
self.patientAge = patientAge
self.patientDOB = patientDOB
self.patientPhone = patientPhone
self.patientAddress = patientAddress
self.facilityName = facilityName
self.facilityRoom = facilityRoom
self.facilityFloor = facilityFloor
self.facilityPhoneNumber = facilityPhoneNumber
}
init() {}
}
Content View
struct ContentView: View
{
#ObservedObject var patient = PatientData()
...
}
Note that:
PatientData()
is an equivalent of:
PatientData.init()
Which means that if you want to create your PatientData this way:
#ObservedObject var patient = PatientData()
you need to provide a matching init method (it can be empty as all your #Published properties have already a default value):
init() { }
EDIT
Looking at your full code, it seems like one of your properties has no initial value:
#Published var patientAddress: PatientAddress
An empty init will work only when all your properties are already initialised, which means you need to assign some value to your patientAddress:
#Published var patientAddress = PatientAddress()
However, for this to work, you'd need to add an empty init in PatientAddress as well:
struct PatientAddress {
...
init() {}
}
NOTE
All your code could be much simpler without all these initialisers. If you only use empty init methods you don't have to declare them (they are auto-generated for structs if there are no other initialisers):
class PatientData: Identifiable, ObservableObject {
let id = UUID()
#Published var patientName: String = "Name"
#Published var patientAge: String = "Age"
#Published var patientDOB: String = "DOB"
#Published var patientPhone: String = "Phone"
#Published var patientAddress: PatientAddress = PatientAddress()
#Published var facilityName: String = "Facility"
#Published var facilityRoom: String = "Room Number"
#Published var facilityFloor: String = "Floor"
#Published var facilityPhoneNumber: String = "Phone Number"
}
extension PatientData {
struct PatientAddress {
var patientStreetAddress: String = "Street"
var patientCity: String = "City"
var patientState: String = "State"
var patientZip: String = "Zip"
}
}
Also, Swift can infer types automatically. You don't usually have to declare types explicitly:
#Published var patientPhone = "Phone"
#Published var patientAddress = PatientAddress()

ForEach TextField in SwiftUI

Let's say that I have a class Student
class Student: Identifiable, ObservableObject {
var id = UUID()
#Published var name = ""
}
Used within an Array in another class (called Class)
class Class: Identifiable, ObservableObject {
var id = UUID()
#Published var name = ""
var students = [Student()]
}
Which is defined like this in my View.
#ObservedObject var newClass = Class()
My question is: how can I create a TextField for each Student and bind it with the name property properly (without getting errors)?
ForEach(self.newClass.students) { student in
TextField("Name", text: student.name)
}
Right now, Xcode is throwing me this:
Cannot convert value of type 'TextField<Text>' to closure result type '_'
I've tried adding some $s before calling the variables, but it didn't seem to work.
Simply change the #Published into a #State for the Student's name property. #State is the one that gives you a Binding with the $ prefix.
import SwiftUI
class Student: Identifiable, ObservableObject {
var id = UUID()
#State var name = ""
}
class Class: Identifiable, ObservableObject {
var id = UUID()
#Published var name = ""
var students = [Student()]
}
struct ContentView: View {
#ObservedObject var newClass = Class()
var body: some View {
Form {
ForEach(self.newClass.students) { student in
TextField("Name", text: student.$name) // note the $name here
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
In general I'd also suggest to use structs instead of classes.
struct Student: Identifiable {
var id = UUID()
#State var name = ""
}
struct Class: Identifiable {
var id = UUID()
var name = ""
var students = [
Student(name: "Yo"),
Student(name: "Ya"),
]
}
struct ContentView: View {
#State private var newClass = Class()
var body: some View {
Form {
ForEach(self.newClass.students) { student in
TextField("Name", text: student.$name)
}
}
}
}

Swift Array .append() method not working in SwiftUI

I'm struggling to do a simple append in SwiftUI. Here's my code:
// This is defined in my custom view
var newClass = Class()
// This is inside a List container (I hid the Button's content because it doesn't matter)
Button(action: {
self.newClass.students.append(Student())
print(self.newClass.students) // This prints an Array with only one Student() instance - the one defined in the struct's init
})
// These are the custom structs used
struct Class: Identifiable {
var id = UUID()
#State var name = ""
#State var students: [Student] = [Student()] // Right here
}
struct Student: Identifiable {
var id = UUID()
#State var name: String = ""
}
I think it might be somehow related to the new #Struct thing, but I'm new to iOS (and Swift) development, so I'm not sure.
Let's modify model a bit...
struct Class: Identifiable {
var id = UUID()
var name = ""
var students: [Student] = [Student()]
}
struct Student: Identifiable {
var id = UUID()
var name: String = ""
}
... and instead of using #State in not intended place (because it is designed to be inside View, instead of model), let's introduce View Model layer as
class ClassViewModel: ObservableObject {
#Published var newClass = Class()
}
and now we can declare related view that behaves as expected
struct ClassView: View {
#ObservedObject var vm = ClassViewModel()
var body: some View {
Button("Add Student") {
self.vm.newClass.students.append(Student())
print(self.vm.newClass.students)
}
}
}
Output:
Test[4298:344875] [Agent] Received display message [Test.Student(id:
D1410829-F039-4D15-8440-69DEF0D55A26, name: ""), Test.Student(id:
50D45CC7-8144-49CC-88BE-598C890F2D4D, name: "")]

Resources