Load UserDefault and loop them - swift

I have this Struct
struct DispItem: Identifiable, Codable {
let id = UUID()
let name: String
}
which I have in UserDefaults by this way:
init() {
if let items = UserDefaults.standard.data(forKey: "DispItems") {
let decoder = JSONDecoder()
if let decoded = try? decoder.decode([DispItem].self, from: items) {
self.items = decoded
return
}
}
self.items = []
}
Inside a "Dispense" class.
Inside another Swift View file I would like to load this UserDefaults "DispItems" and cycle the values.
I am still at the beginning of Swift/SwiftUI and the code I used to store it was taken from a couple of tutorial.
I assume that if I load it by doing so:
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(items) {
UserDefaults.standard.set(encoded, forKey: "Items")
}
But I assume that "items" will contain a Struct; how can I take the values and display them inside a ForEach loop?
Thanks
Marco

I like to use UserDefaults.standard.set(try? PropertyListEncoder().encode(items), forKey: "DispItems"). This way you can encode, and then you can decode by using
typealias DispItems = [DispItem]
guard let data = UserDefaults.standard.object(forKey: "player") as? Data else {
return
}
guard let dispItems = try? PropertyListDecoder().decode(DispItems.self, from: data) else {
return
}
dispItems.forEach{
//Do what you'd like with the items
}

Related

Prevent lost of data on AppStorage when changing a struct

I have a data model that handles a structure and the data the app uses. I'm saving that data using AppStorage.
I recently needed to add an extra value to the struct, and when I did that, all the data saved was gone.
is there any way to prevent this? I can't find anything on Apple's documentation, or other Swift or SwiftUI sites about this.
Here's my data structure and how I save it.
let dateFormatter = DateFormatter()
struct NoteItem: Codable, Hashable, Identifiable {
let id: UUID
var text: String
var date = Date()
var dateText: String {
dateFormatter.dateFormat = "EEEE, MMM d yyyy, h:mm a"
return dateFormatter.string(from: date)
}
var tags: [String] = []
//var starred: Int = 0 // if I add this, it wipes all the data the app has saved
}
final class DataModel: ObservableObject {
#AppStorage("myappdata") public var notes: [NoteItem] = []
init() {
self.notes = self.notes.sorted(by: {
$0.date.compare($1.date) == .orderedDescending
})
}
func sortList() {
self.notes = self.notes.sorted(by: {
$0.date.compare($1.date) == .orderedDescending
})
}
}
extension Array: RawRepresentable where Element: Codable {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode([Element].self, from: data)
else {
return nil
}
self = result
}
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else {
return "[]"
}
return result
}
}
I certainly agree that UserDefaults (AppStorage) is no the best choice for this but whatever storage solution you choose you are going to need a migration strategy. So here are two routes you can take to migrate a changed json struct.
The first one is to add a custom init(from:) to your struct and handle the new property separately
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(UUID.self, forKey: .id)
text = try container.decode(String.self, forKey: .text)
date = try container.decode(Date.self, forKey: .date)
tags = try container.decode([String].self, forKey: .tags)
if let value = try? container.decode(Int.self, forKey: .starred) {
starred = value
} else {
starred = 0
}
}
The other option is to keep the old version of the struct with another name and use it if the decoding fails for the ordinary struct and then convert the result to the new struct
extension NoteItem {
static func decode(string: String) -> [NoteItem]? {
guard let data = string.data(using: .utf8) else { return nil }
if let result = try? JSONDecoder().decode([NoteItem].self, from: data) {
return result
} else if let result = try? JSONDecoder().decode([NoteItemOld].self, from: data) {
return result.map { NoteItem(id: $0.id, text: $0.text, date: $0.date, tags: $0.tags, starred: 0)}
}
return nil
}
}

How to store user data locally in SwiftUI

I try to accomplish having an observable object with a published value training. On every change it should save the custom struct to the user defaults. On every load (AppState init) it should load the data:
class AppState: ObservableObject {
var trainings: [Training] {
willSet {
if let encoded = try? JSONEncoder().encode(trainings) {
let defaults = UserDefaults.standard
defaults.set(encoded, forKey: "trainings")
}
objectWillChange.send()
}
}
init() {
self.trainings = []
if let savedTrainings = UserDefaults.standard.object(forKey: "trainings") as? Data {
if let loadedTraining = try? JSONDecoder().decode([Training].self, from: savedTrainings) {
self.trainings = loadedTraining
}
}
}
}
I know if this is best practice, but I want to save the data locally.
The code I wrote is not working and I can't figure out why.
I'm a beginner and I never stored data to a device.
Each time you call the init method the first line resets the value stored in UserDefaults and in-turn returns the empty array instead of the value that was previously stored. Try this modification to your init method to fix it:
init() {
if let savedTrainings = UserDefaults.standard.object(forKey: "trainings") as? Data,
let loadedTraining = try? JSONDecoder().decode([Training].self, from: savedTrainings) {
self.trainings = loadedTraining
} else {
self.trainings = []
}
}
Better Approach: A much better approach would to modify your trainings property to have a get and set instead of the current setup. Here is an example:
var trainings: [Training] {
set {
if let encoded = try? JSONEncoder().encode(newValue) {
let defaults = UserDefaults.standard
defaults.set(encoded, forKey: "trainings")
}
objectWillChange.send()
}
get {
if let savedTrainings = UserDefaults.standard.object(forKey: "trainings") as? Data,
let loadedTraining = try? JSONDecoder().decode([Training].self, from: savedTrainings) {
return loadedTraining
}
return []
}
}
Note: This can again be improved using Swift 5.1's #PropertyWrapper. Let me know in the comments if anyone wants me to include that as well in the answer.
Update: Here's the solution that makes it simpler to use UserDefaults using Swift's #PropertyWrapper as you have requested for:-
#propertyWrapper struct UserDefault<T: Codable> {
var key: String
var wrappedValue: T? {
get {
if let data = UserDefaults.standard.object(forKey: key) as? Data {
return try? JSONDecoder().decode(T.self, from: data)
}
return nil
}
set {
if let encoded = try? JSONEncoder().encode(newValue) {
UserDefaults.standard.set(encoded, forKey: key)
}
}
}
}
class AppState: ObservableObject {
#UserDefault(key: "trainings") var trainings: [Training]?
#UserDefault(key: "anotherProperty") var anotherPropertyInUserDefault: AnotherType?
}

Saving a Codable Struct to UserDefaults with Swift

I am trying to encode a struct
struct Configuration : Encodable, Decodable {
private enum CodingKeys : String, CodingKey {
case title = "title"
case contents = "contents"
}
var title : String?
var contents: [[Int]]?
}
into JSON to store in a local key of UserDefaults.standard. I have the following code:
let jsonString = Configuration(title: nameField.text, contents: newContents)
let info = ["row" as String: jsonString as Configuration]
print("jsonString = \(jsonString)")
//trying to save object
let defaults = UserDefaults.standard
let recode = try! JSONEncoder().encode(jsonString)
defaults.set(recode, forKey: "simulationConfiguration")
//end of saving local
The print returns:
jsonString = Configuration(title: Optional("config"), contents: Optional([[4, 5], [5, 5], [6, 5]]))
so I believe I am creating the object correctly. However, when I try and retrieve the key the next time I run the simulator I get nothing.
I put the following in AppDelegate and it always returns No Config.
let defaults = UserDefaults.standard
let config = defaults.string(forKey: "simulationConfiguration") ?? "No Config"
print("from app delegate = \(config.description)")
Any ideas? Thanks
Here you are saving a Data value (which is correct)
defaults.set(recode, forKey: "simulationConfiguration")
But here you are reading a String
defaults.string(forKey: "simulationConfiguration")
You cannot save Data, read String and expect it to work.
Let's fix your code
First of all you don't need to manually specify the Coding Keys. So your struct become simply this
struct Configuration : Codable {
var title : String?
var contents: [[Int]]?
}
Saving
Now here's the code for saving it
let configuration = Configuration(title: "test title", contents: [[1, 2, 3]])
if let data = try? JSONEncoder().encode(configuration) {
UserDefaults.standard.set(data, forKey: "simulationConfiguration")
}
Loading
And here's the code for reading it
if
let data = UserDefaults.standard.value(forKey: "simulationConfiguration") as? Data,
let configuration = try? JSONDecoder().decode(Configuration.self, from: data) {
print(configuration)
}
encode(_:) function of JSONEncoder returns Data, not String. This means when you need to get the Configuration back from UserDefaults you need to get data and decode them.
Here is example:
let defaults = UserDefaults.standard
guard let configData = defaults.data(forKey: "simulationConfiguration") else {
return nil // here put something or change the control flow to if statement
}
return try? JSONDecoder().decode(Configuration.self, from: configData)
you also don't need to assign value to all the cases in CodingKeys, the values is automatically the name of the case
if you are conforming to both, Encodable and Decodable, you can simply use Codable instead as it is combination of both and defined as typealias Codable = Encodable & Decodable
If you want an external dependency that saves a boat load of frustration, checkout SwifterSwift
Here's how I did it in two lines using their UserDefaults extension.
For setting:
UserDefaults.standard.set(object: configuration, forKey: "configuration")
For retrieving the object:
guard let configuration = UserDefaults.standard.object(Configuration.self, with: "configuration") else { return }
print(configuration)
That's about it..!!
Basically your UserDefault stored property will be look something like this,
private let userDefaults = UserDefaults.standard
var configuration: Configuration? {
get {
do {
let data = userDefaults.data(forKey: "configuration_key")
if let data {
let config = try JSONDecoder().decode(User.self, from: data)
return config
}
} catch let error {
print("Preference \(#function) json decode error: \(error.localizedDescription)")
}
return nil
} set {
do {
let data = try JSONEncoder().encode(newValue)
userDefaults.set(data, forKey: "configuration_key")
} catch let error {
print("Preference \(#function) json encode error: \(error.localizedDescription)")
}
}
}

How to save an Array with Objects to UserDefaults

My Object conforms to the new Swift 4 Codeable protocol. How to save an array of these Objects in UserDefaults?
struct MyObject: Codeable {
var name: String
var something: [String]
}
myObjectsArray = [MyObject]() // filled with objects
UserDefaults.standard.set(myObjectsArray, forKey: "user_filters")
Error
Terminating app due to uncaught exception
'NSInvalidArgumentException', reason: 'Attempt to insert non-property
list object
Whew, I got it working:
Here is the Swift 4 Syntax to save an Array with Codeable Objects:
My solution is to encode it as a JSON Object and save that:
static var getAllObjects: [MyObject] {
let defaultObject = MyObject(name: "My Object Name")
if let objects = UserDefaults.standard.value(forKey: "user_objects") as? Data {
let decoder = JSONDecoder()
if let objectsDecoded = try? decoder.decode(Array.self, from: objects) as [MyObject] {
return objectsDecoded
} else {
return [defaultObject]
}
} else {
return [defaultObject]
}
}
static func saveAllObjects(allObjects: [MyObject]) {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(allObjects){
UserDefaults.standard.set(encoded, forKey: "user_objects")
}
}
You can use a more generic approach, using array with specific type:
(myObject = any custom codable object you like)
(myKey = a string constant key to be able to retrieve/set specific array)
//set
setObject(myArray, forKey: mykey)
//get
let myArray = getObject(forKey: mykey, castTo: Array<myObject>.self)
and generic functions also, for any type:
func setObject<Object>(_ object: Object, forKey: String) where Object: Encodable
{
let encoder = JSONEncoder()
do {
let data = try encoder.encode(object)
set(data, forKey: forKey)
synchronize()
} catch let encodeErr {
print("Failed to encode object:", encodeErr)
}
}
func getObject<Object>(forKey: String, castTo type: Object.Type) -> Object? where Object: Decodable
{
guard let data = data(forKey: forKey) else { return nil }
let decoder = JSONDecoder()
do {
let object = try decoder.decode(type, from: data)
return object
} catch let decodeError{
print("Failed to decode object:" , decodeError)
return nil
}
}

How to compare two values in Realm Swift

I have a Realm database called NewsCount. I need to download a new news only if there is a new news (respectively when newsCount change). And I make a comparison with the data parsing. But I can not compare them properly. How do you compare them?
Thi is my code
private func parseJSONData(_ data: Data) {
do {
let temp: NSString = NSString(data: data, encoding: String.Encoding.utf8.rawValue)!
let myNSData = temp.data(using: String.Encoding.utf8.rawValue)!
guard let jsonResult = try JSONSerialization.jsonObject(with: myNSData, options: JSONSerialization.ReadingOptions.mutableContainers) as? NSDictionary else {
return
}
guard let jsonNews = jsonResult["categories"] as? [AnyObject] else {
print("Empty array")
return
}
let realm = try Realm()
let category = realm.objects(NewsCount.self)
var array = [Int]()
for i in category {
array.append(i.newsCount)
}
print(array)
print("News COUNT2 \(category)")
for jsonnewes in jsonNews {
let newsJson = NewsCount()
//HERE I COMPARE
if !UserDefaults.standard.bool(forKey: "AppStarted") || jsonnewes["count"] as! Int > array[jsonnewes as! Int]{
newsJson.newsID = jsonnewes["term_id"] as! Int
newsJson.newsCount = jsonnewes["count"] as! Int
//print("News COUNT2 \(newsJson.newsCount)")
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "downloadNew"), object: nil)
} else {
newsJson.newsID = jsonnewes["term_id"] as! Int
newsJson.newsCount = jsonnewes["count"] as! Int
//print("News COUNT3 \(newsJson.newsCount)")
}
insertOrUpdate(newsJson)
}
} catch {
print(error)
}
}
Because Realm uses RealmOptional to use Int values you have to call the value atribute to the RealmOptional
Try change this:
for i in category {
array.append(i.newsCount.value)
}
First off, it's probably more appropriate to use Int(string) instead of as! Int force-casting to convert your JSON data to integers.
From the looks of it, jsonnewes is a dictionary full of JSON data, but you're casting it as an array index in array[jsonnewes as! Int] which (given array is an array and not a dictionary) shouldn't work.
Instead, in order to make sure you're explicitly retrieving the item you want, I'd recommend using Realm's primary key query method in order to retrieve the item you want.
let realm = try! Realm()
for newsItem in jsonNews {
let newsPrimaryKey = Int(newsItem)
let realmNews = realm.object(ofType: NewsCount.self, forPrimaryKey: newsPrimaryKey)
// Don't continue if a Realm object couldn't be found
guard let realmNews = realmNews else {
continue
}
// Perform comparison
if Int(newsItem["count"]) > realmNews.newsCount {
// Perform the update
}
}