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
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()
}
}
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.
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()
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)
}
}
}
}
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: "")]