I'm struggling with saving some date to UserDefaults. I have a struct, an array of which I'm going to save:
struct Habit: Identifiable, Codable {
var id = UUID()
var name: String
var comments: String
}
Then, in the view, I have a button to save new habit to an array of habits and put it into UserDefaults:
struct AddView: View {
#State private var newHabit = Habit(name: "", comments: "")
#State private var name: String = ""
let userData = defaults.object(forKey: "userData") as? [Habit] ?? [Habit]()
#State private var allHabits = [Habit]()
var body: some View {
NavigationView {
Form {
Section(header: Text("Habit name")) {
TextField("Jogging", text: $newHabit.name)
}
Section(header: Text("Description")) {
TextField("Brief comments", text: $newHabit.comments)
}
}
.navigationBarTitle("New habit")
.navigationBarItems(trailing: Button(action: {
allHabits = userData
allHabits.append(newHabit)
defaults.set(allHabits, forKey: "userData")
}) {
addButton
})
}
}
}
When I tap the button, my app crashes with this thread: Thread 1: "Attempt to insert non-property list object (\n \"HabitRabbit.Habit(id: 574CA523-866E-47C3-B56B-D0F85EBD9CB1, name: \\\"Wfs\\\", comments: \\\"Sdfdfsd\\\")\"\n) for key userData"
What did I do wrong?
Adopting Codable doesn't make the object property list compliant per se, you have to encode and decode the object to and from Data.
Something like this
func loadData() -> [Habit]
guard let userData = defaults.data(forKey: "userData") else { return [] }
return try? JSONDecoder().decode([Habit].self, from: userData) ?? []
}
func saveData(habits : [Habit]) {
guard let data = try? JSONEncoder().encode(habits) else { return }
defaults.set(data, forKey: "userData")
}
Related
I have a problem that Firebase not saving the data and it shows like that :
enter image description here
First I created a model :
struct Box: Identifiable, Hashable, Codable {
#DocumentID var id: String?
var boxName: String
var boxSize: String
enum CodingKeys: String, CodingKey {
case id
case boxName
case boxSize
}
var dictionary: [String: Any] {
let data = (try? JSONEncoder().encode(self)) ?? Data()
return (try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any]) ?? [:]
}
}
Then I created ViewModel:
class BoxViewModel: ObservableObject {
#Published var box: Box
private var db = Firestore.firestore()
init(box: Box = CashBox(boxName: "", boxSize: "")) {
self.box = box
}
private func addNewBox(_ box: Box){
do {
print("the name \(box.boxName)")
let _ = db.collection("Box")
.addDocument(data: box.dictionary)
}
}
func addBox() {
self.addNewBox(box)
}
func setBoxData(boxName: String, boxSize: String){
self.box.boxName = boxName
self.box.boxSize = boxSize
}
}
Finally here is the view:
struct CashBoxView: View {
#State var boxName = ""
#State var boxSize = ""
#EnvironmentObject var boxViewModel: BoxViewModel
var body: some View {
Text("Enter box name")
TextField("", text: $boxName)
Text("Enter box size")
TextField("", text: $boxSize)
Button( action: {
boxViewModel.setBoxData(boxName: boxName, boxSize: boxSize)
boxViewModel.addBox()
}) {
Text("Done")
}
}
First I thought that the problem is the box is empty but when I tried to print the box name print("the name \(box.boxName)") and printed it
is the problem the the viewModel is #EnvironmentObject ? or what is the problem ?
Thank you,
This is Model and View Model. I am using UserDefaults for saving data.
import Foundation
struct Item: Identifiable, Codable {
var id = UUID()
var name: String
var int: Int
var date = Date()
}
class ItemViewModel: ObservableObject {
#Published var ItemList = [Item] ()
init() {
load()
}
func load() {
guard let data = UserDefaults.standard.data(forKey: "ItemList"),
let savedItems = try? JSONDecoder().decode([Item].self, from: data) else { ItemList = []; return }
ItemList = savedItems
}
func save() {
do {
let data = try JSONEncoder().encode(ItemList)
UserDefaults.standard.set(data, forKey: "ItemList")
} catch {
print(error)
}
}
}
and this is the view. I am tryng too add new item and sort them by date. After that adding numbers on totalNumber. I tried .sorted() in ForEach but its not work for sort by date. and I try to create a func for adding numbers and that func is not work thoo.
import SwiftUI
struct ContentView: View {
#State private var name = ""
#State private var int = 0
#AppStorage("TOTAL_NUMBER") var totalNumber = 0
#StateObject var VM = ItemViewModel()
var body: some View {
VStack(spacing: 30) {
VStack(alignment: .leading) {
HStack {
Text("Name:")
TextField("Type Here...", text: $name)
}
HStack {
Text("Number:")
TextField("Type Here...", value: $int, formatter: NumberFormatter())
}
Button {
addItem()
VM.save()
name = ""
int = 0
} label: {
Text ("ADD PERSON")
}
}
.padding()
VStack(alignment: .leading) {
List(VM.ItemList) { Item in
Text(Item.name)
Text("\(Item.int)")
Text("\(Item.date, format: .dateTime.day().month().year())")
}
Text("\(totalNumber)")
.padding()
}
}
}
func addItem() {
VM.ItemList.append(Item(name: name, int: int))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
First of all please name variables always with starting lowercase letter for example
#Published var itemList = [Item] ()
#StateObject var vm = ItemViewModel()
To sort the items by date in the view model replace
itemList = savedItems
with
itemList = savedItems.sorted{ $0.date < $1.date }
To show the sum of all int properties of the today items add a #Published var totalNumber in the view model and a method to calculate the value. Call this method in load and save
class ItemViewModel: ObservableObject {
#Published var itemList = [Item] ()
#Published var totalNumber = 0
init() {
load()
}
func load() {
guard let data = UserDefaults.standard.data(forKey: "ItemList"),
let savedItems = try? JSONDecoder().decode([Item].self, from: data) else { itemList = []; return }
itemList = savedItems.sorted{ $0.date < $1.date }
calculateTotalNumber()
}
func save() {
do {
let data = try JSONEncoder().encode(itemList)
UserDefaults.standard.set(data, forKey: "ItemList")
calculateTotalNumber()
} catch {
print(error)
}
}
func calculateTotalNumber() {
let todayItems = itemList.filter{ Calendar.current.isDateInToday($0.date) }
totalNumber = todayItems.map(\.int).reduce(0, +)
}
}
In the view delete the #AppStorage line because the value is calculated on demand and replace
Text("\(totalNumber)")
with
Text("\(vm.totalNumber)")
I'm fairly new to SwiftUI and I'm trying to update the details on a list and then save it.
I am able to get the details to update, but every time I try saving I'm not able to do it.
I have marked the area where I need help. Thanks in advance.
// This is the Array to store the items:
struct ExpenseItem : Identifiable, Codable {
var id = UUID()
let name: String
let amount: Int
}
// This is the UserDefault Array
class Expenses: ObservableObject {
#Published var items = [ExpenseItem]() {
didSet {
if let encoded = try? JSONEncoder().encode(items) {
UserDefaults.standard.set(encoded, forKey: "Items")
}
}
}
init() {
if let savedItems = UserDefaults.standard.data(forKey: "Items") {
if let decodedItems = try? JSONDecoder().decode([ExpenseItem].self, from: savedItems) {
items = decodedItems
return
}
}
items = []
}
}
// View to add details :
struct AddView: View {
#State private var name = ""
#State private var amount = 0
#StateObject var expenses: Expenses
#Environment(\.dismiss) var dismiss
var body: some View {
Form {
TextField("Name", text: $name)
Text("\(amount)")
Button("Tap Me") {
amount += 1
}
}
.navigationTitle("Add New Count")
.toolbar {
if name != "" {
Button("Save") {
let item = ExpenseItem(name: name, amount: amount)
expenses.items.append(item)
dismiss()
}
}
}
}
}
// This is the file to update the details:
struct UpdateDhikr: View {
#EnvironmentObject var expenses : Expenses
#State var name : String
#State var amount : Int
var body: some View {
Form {
TextField("Name", text: $name)
Text("\(amount)")
Button("Tap Me") {
amount += 1
}
}
.navigationTitle("Update Count")
.toolbar {
if name != "" {
Button("Save") {
// This is where I'm having problems.
}
}
}
}
}
So I have tried for a couple of hours now and it is just not working. I have my firebase Firestore setup and have a collection called leaderboard, which is supposed to store users with their score. However, when I fetch this data and display it, my screen is blank.
Firebase
LeaderBoardModel
import Foundation
import Firebase
class LeaderBoardModel: ObservableObject{
#Published var users = [LeaderBoardItem]()
func getData(){
let db = Firestore.firestore()
db.collection("leaderboard").getDocuments { snapshot, error in
if error == nil{
if let snapshot = snapshot{
DispatchQueue.main.async {
self.users = snapshot.documents.map { d in
return LeaderBoardItem(id: d.documentID, score: d["score"] as? String ?? "", name: d["name"] as? String ?? "", email: d["email"] as? String ?? "")
}
}
}
}
else{
}
}
}
}
LeaderBoardView
import SwiftUI
struct LeaderBoardView: View {
#ObservedObject var leaderBoardModel = LeaderBoardModel()
var body: some View {
List(leaderBoardModel.users) { item in
Text(item.name)
}
}
}
LeaderBoardItem
import Foundation
struct LeaderBoardItem: Identifiable{
var id: String
var score: String
var name: String
var email: String
}
try this approach, using .onAppear{...}:
struct LeaderBoardView: View {
#StateObject var leaderBoardModel = LeaderBoardModel() // <-- here
var body: some View {
List(leaderBoardModel.users) { item in
Text(item.name)
}.onAppear {
leaderBoardModel.getData() // <-- here
}
}
}
Hi have a question to the following code:
struct ContentView: View {
#State private var listItems = [Item]()
let savedItems = UserDefaults.standard.string(forKey: "Items") ?? "error"
var body: some View {
NavigationView{
List{
ForEach(listItems){ item in
Text(item.name)
}
NavigationLink(
destination: AddView(listItems: $listItems),
label: {
Text(" Add")
.foregroundColor(.blue)
})
}
.navigationBarTitle("MyApp")
}
}
}
struct AddView: View {
#State var text:String = ""
#Binding var listItems: [Item]
var body: some View {
VStack{
TextField("Name", text: $text).padding().background(Color(.systemGray5)).frame(width: 400,alignment: .center).cornerRadius(20)
Button(action: {
listItems.append(Item(name: text, image: "Gif"))
print(listItems)
UserDefaults.standard.set(listItems, forKey: "Items")
}, label: {
Text("Add")
})
}
}
}
If i append something in the AddView, i want to save it in the UserDefaults and append it automatically when the app starts. But I don't know how to do this. Can you help me ?
PS: I'm using AppDelegate and SceneDelegate.
To save something in the UserDefaults it will need to be either of those things:
The value parameter can be only property list objects: NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. For NSArray and NSDictionary objects, their contents must be property list objects.
see full documentation
You are trying assign and array of Item objects, and while it is perfectly fine to save an array, the Element type of this array will also need to be one of the types mentioned in the documentation.
Your problem can be solved using Codable conformance.
Extend your item type with Codable
struct Item: Codable {
/// your implementation
}
Save encoded items to UserDefaults
extension UserDefaults {
func saveItems(_ items: [Item]) {
let encoder = JSONEncoder()
if let data = try? encoder.encode(items) {
set(data, forKey: "Items")
}
}
}
simply call this method anytime you want to save listItems array.
UserDefaults.standard.saveItems(listItems)
Read stored items from UserDefaults
extension UserDefaults {
func readItems() -> [Item]? {
guard let data = value(forKey: "Items") as? Data else {
return nil
}
let decoder = JSONDecoder()
return try? decoder.decode([Item].self, from: data)
}
}
with this method you can lazily load items when you need them
#State private var listItems = UserDefaults.standard.readItems() ?? []