I am trying to figure out how to make two things work together. The first is persistent data using userdefaults, which I have figured out by using @Published and @Observable, then using JSONencoder/decoder to get that data to save even when the app is closed and reopened. The second I have also recently learned via this previous question < https://stackoverflow.com/questions/62106227/best-way-to-update-edit-an-array-element-in-swiftui/67752060#67752060>
My issue is combining the two. I can’t seem to figure out how to take the below code with predefined array of data and make the changes persist using UserDefaults. Any help would be greatly appreciated. Maybe I’m approaching this incorrectly.
Code for persistent data via UserDefaults
import SwiftUI
struct CharacterModel: Identifiable, Codable {
var id = UUID()
var name: String
var level: Int
}
class CharacterViewModel: ObservableObject {
@Published var characters = [CharacterModel]() {
// Write data back to Model
didSet {
let encoder = JSONEncoder()
if let encoded = try?
encoder.encode(characters) {
UserDefaults.standard.set(encoded, forKey: "Characters")
}
}
}
init() {
if let characters = UserDefaults.standard.data(forKey: "Characters") {
let decoder = JSONDecoder()
if let decoded = try?
decoder.decode([CharacterModel].self, from: characters) {
self.characters = decoded
return
}
}
self.characters = []
}
}
struct DetailView: View {
@Environment(\.presentationMode) var presentationMode
@ObservedObject var characterVM: CharacterViewModel
@State private var name = ""
@State private var level = ""
var body: some View {
NavigationView {
Form {
TextField("Name", text: $name)
TextField("Level", text: $level)
.keyboardType(.numberPad)
}
.navigationBarTitle("Add Level")
.navigationBarItems(trailing: Button("Save") {
if let actualLevel = Int(self.level) {
let character = CharacterModel(name: self.name, level: actualLevel)
self.characterVM.characters.append(character)
self.presentationMode.wrappedValue.dismiss()
}
})
}
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(characterVM: CharacterViewModel())
}
}
struct Home: View {
@State private var showingAddCharacter = false
@State var selectedCharacter : CharacterModel!
@ObservedObject var characterVM = CharacterViewModel()
var body: some View {
NavigationView {
List {
ForEach(characterVM.characters) { char in
HStack {
VStack(alignment: .leading) {
Text(char.name)
.font(.headline)
}
Spacer()
Text("\(char.level)")
}
}
.onDelete(perform: removeItems)
}
.navigationBarTitle("Characters")
.navigationBarItems(trailing:
Button(action: {
self.showingAddCharacter = true
}) {
Image(systemName: "plus")
}
)
.sheet(isPresented: $showingAddCharacter) {
DetailView(characterVM: self.characterVM)
}
}
}
func removeItems(at offsets: IndexSet) {
characterVM.characters.remove(atOffsets: offsets)
}
}
struct Home_Previews: PreviewProvider {
static var previews: some View {
Home()
}
}
Editing a predefined array
class Training: ObservableObject, Identifiable {
let id: String
@Published var trainingName: String
@Published var isRequired: Bool
init(id: String, trainingName: String, isRequired: Bool) {
self.id = id
self.trainingName = trainingName
self.isRequired = isRequired
}
}
class GetTrainings: ObservableObject {
@Published var items = [Training]()
init() {
self.items = [
Training(id: "ttt1", trainingName: "Safety", isRequired: true),
Training(id: "ttt2", trainingName: "Administrative", isRequired: false),
Training(id: "ttt3", trainingName: "Computer", isRequired: true),
Training(id: "ttt4", trainingName: "People", isRequired: true),
Training(id: "ttt5", trainingName: "Managerial", isRequired: true),
]
}
}
struct TrainingList: View {
@ObservedObject var trainings = GetTrainings()
var body: some View {
NavigationView {
VStack {
List {
ForEach(trainings.items) { training in
HStack {
NavigationLink(destination: TrainingView(training: training)) {
Text("\(training.trainingName)")
}
}
}
}
}.navigationBarTitle("Training List")
.onAppear {
self.trainings.objectWillChange.send() // refresh
}
}
}
}
struct TrainingView: View {
@ObservedObject var training: Training
var body: some View {
VStack {
Text("\(training.trainingName)").font(.body)
Text("\(training.isRequired == true ? "Required Training" : "Training Not Required")")
HStack {
NavigationLink(destination: EditTraining(training: training)) {
Text("Edit Training Details")
}
}
}.navigationBarTitle("\(training.trainingName) Page", displayMode: .inline)
}
}
struct EditTraining: View {
@ObservedObject var training: Training
@State private var newName: String
@State private var isRequiredTraining: Bool
init(training: Training) {
self.training = training
self._newName = State(initialValue: training.trainingName)
self._isRequiredTraining = State(initialValue: training.isRequired)
}
private func submitData() {
let newName = self.newName
let newBoolVal = self.isRequiredTraining
print("Firebase Sync Id is :\(training.id) Text: \(newName) and Bool: \(newBoolVal)")
self.training.trainingName = newName
self.training.isRequired = newBoolVal
}
var body: some View {
VStack {
Form {
Section (header: Text("Edit")) {
Text("\(training.trainingName)")
/* TextField should Populate With passed In Training Name Here*/
TextField("New Name", text: self.$newName)
Toggle(isOn: self.$isRequiredTraining) {
Text("Is Required")
}
}
Section {
Button(action: {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder),
to:nil, from:nil, for:nil)
self.submitData()
}) {
Text("Submit")
}
}
}
}.navigationBarTitle("Edit Training Page", displayMode: .inline)
}
}