SwiftUI: keep data even after closing app - swift

i have been trying to make that when a user adds a page to favorites or removes the page it saves it, so when a user closes the app it remembers it. I can't figure out how i can save the mushrooms table. I want to save it locally and is it done by using Prospects ?
class Favorites: ObservableObject {
public var mushrooms: Set<String>
public let saveKey = "Favorites"
init() {
mushrooms = []
}
func contains(_ mushroom: Mushroom) -> Bool {
mushrooms.contains(mushroom.id)
}
func add (_ mushroom: Mushroom) {
objectWillChange.send()
mushrooms.insert(mushroom.id)
save()
}
func remove(_ mushroom: Mushroom) {
objectWillChange.send()
mushrooms.remove(mushroom.id)
save()
}
func save() {
}
}

I was able to figure it out. Here is the code i did if someone else is struggling with this.
I added this to the save function
func save() {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(mushrooms) {
defaults.set(encoded, forKey: "Favorites")
}
}
And to the init() :
let decoder = JSONDecoder()
if let data = defaults.data(forKey: "Favorites") {
let mushroomData = try? decoder.decode(Set<String>.self, from: data)
self.mushrooms = mushroomData ?? []
} else {
self.mushrooms = []
}
EDIT:
and of course add the defaults
let defaults = UserDefaults.standard

Related

Best way to use UserDefaults with ObservableObject in Swiftui

I'm trying to save user basic's data in UserDefaults.
My goal is to be able to consume data from UserDefaults and to update them each time the user do some changes.
I'm using an ObservableObject class to set and get these data
class SessionData : ObservableObject {
#Published var loggedInUser: User = User(first_name: "", last_name: "", email: "")
static let shared = SessionData()
func setLoggedInUser (user: User) {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(user) {
UserDefaults.standard.set(encoded, forKey: "User")
self.loggedInUser = currentUser
}
}
and also
struct ProfileView: View {
#ObservedObject var sessionData: SessionData = SessionData.shared
var body: some View {
VStack{
Text(self.sessionData.loggedInUser.first_name)
}
}
}
This way the changes are updated. But if I leave the app I will lose the data.
Solution 2:
I also tried to rely on reading the data from UserDefault like this
class SessionData : ObservableObject {
func getLoggedInUser() -> User? {
if let currentUser = UserDefaults.standard.object(forKey: "User") as? Data {
let decoder = JSONDecoder()
if let loadedUser = try? decoder.decode(User.self, from: currentUser) {
return loadedUser
}
}
return nil
}
}
Problem: I don't get the updates once a user change something :/
I don't find a nice solution to use both UserDefaults and ObservableObject
in "getLoggedInUser()" you are not updating the published var "loggedInUser".
Try this to do the update whenever you use the function:
func getLoggedInUser() -> User? {
if let currentUser = UserDefaults.standard.object(forKey: "User") as? Data {
let decoder = JSONDecoder()
if let loadedUser = try? decoder.decode(User.self, from: currentUser) {
loggedInUser = loadedUser // <--- here
return loadedUser
}
}
return nil
}
or just simply this:
func getLoggedInUser2() {
if let currentUser = UserDefaults.standard.object(forKey: "User") as? Data {
let decoder = JSONDecoder()
if let loadedUser = try? decoder.decode(User.self, from: currentUser) {
loggedInUser = loadedUser // <--- here
}
}
}
You could also do this to automatically save your User when it changes (instead of using setLoggedInUser):
#Published var loggedInUser: User = User(first_name: "", last_name: "", email: "") {
didSet {
if let encoded = try? JSONEncoder().encode(loggedInUser) {
UserDefaults.standard.set(encoded, forKey: "User")
}
}
}
and use this as init(), so you get back what you saved when you leave the app:
init() {
getLoggedInUser2()
}

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?
}

NSKeyedUnarchiver seems not to be reading anything

I'm trying to write an array of objects using NSKeyedArchiver.
Here some parts from my code:
EventStore.swift - holding the event array:
class EventStore{
private var events: [EventItem] = [EventItem]()
static let sharedStore = EventStore()
private init() {
}
static func getEventFile() -> URL{
let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let file = directory.appendingPathComponent("events.bin")
return file
}
func addEvent(withEvent event:EventItem){
events.append(event)
}
func getEvents()->[EventItem]{
return events
}
}
No the eventItem where I implemented NSCoding:
class EventItem: NSObject, NSCoding {
private var id:Int
private var timestamp:Int64
//Object initialization
init(withId id:Int,withTimestamp timestamp:Int64) {
self.id = id
self.timestamp = timestamp
}
required convenience init?(coder: NSCoder) {
//get value from stored key if exists
guard let id = coder.decodeObject(forKey: "id") as? Int,
let timestamp = coder.decodeObject(forKey: "timestamp") as? Int64
//exit init after decoding if a value is missing
else {
NSLog("Unable to decode event")
return nil
}
self.init(withId:id,withTimestamp:timestamp)
}
func getId()->Int{
return id
}
func getTimestamp()->Int64{
return timestamp
}
//encode values to keys
func encode(with aCoder: NSCoder) {
NSLog("Encoding event")
aCoder.encode(id, forKey: "id")
aCoder.encode(timestamp, forKey: "timestamp")
}
}
Finally when the user tape on a button I'm adding an event into the array and saving it:
var eventStore = EventStore.sharedStore
#IBAction func TakeAction() {
//generate new event
let timestamp = Int64(NSDate().timeIntervalSince1970 * 1000)
let newEvent = EventItem(withId: eventStore.eventCount(), withTimestamp: timestamp)
eventStore.addEvent(withEvent: newEvent)
saveEvents()
//refresh ui
updateTakeText()
}
func saveEvents(){
do{
let data = try NSKeyedArchiver.archivedData(withRootObject: eventStore.getEvents(), requiringSecureCoding: false)
NSLog("Data being written : \(data)")
try data.write(to: EventStore.getEventFile())
NSLog("Write events to file :\(EventStore.getEventFile())")
}catch{
NSLog(error.localizedDescription)
}
}
func loadEvents() {
do{
let data = try Data(contentsOf: EventStore.getEventFile())
NSLog("Data loaded from file path: \(data)")
//try get data else return empty array
let events = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? [EventItem] ?? [EventItem]()
NSLog("Events retrived from file: \(events.count)")
eventStore.setEvents(withEvents:events)
}catch{
NSLog(error.localizedDescription)
}
}
I added a lot of debug and it seems that the encoding and file write are working fine but the decoding fail. It always get nil values.
Any clue?
Thanks in advance
When encoding Int values you have to decode them with coder.decodeInteger(forKey: "xxx")

Save state in Userdefaults

I have a class that saves the state of something, in my case some variable of the ViewController, but sometimes it loads wrong or old data, but I can't figure out why.
Maybe somebody can have a look of my code and see if it makes sense.
class TopFlopState: Codable, PersistenceState {
var group: Groups = .large {
didSet {
save()
}
}
var base: Bases = .usd {
didSet {
save()
}
}
var valueOne: StatIntervalBaseModel = StatIntervalBaseModel(stat: "ppc", interval: "24h", base: "usd") {
didSet {
save()
}
}
init(){
let savedValues = load()
if savedValues != nil {
self.group = savedValues!.group
self.base = savedValues!.base
self.valueOne = savedValues!.valueOne
}
}
}
This is the PersistenceState protocol:
/**
Saves and Loads the class, enum etc. with UserDefaults.
Has to conform to Codable.
Uses as Key, the name of the class, enum etc.
*/
protocol PersistenceState {
}
extension PersistenceState where Self: Codable {
private var keyUserDefaults: String {
return String(describing: self)
}
func save() {
saveUserDefaults(withKey: keyUserDefaults, myType: self)
}
func load() -> Self? {
return loadUserDefaults(withKey: keyUserDefaults)
}
private func saveUserDefaults<T: Codable>(withKey key: String, myType: T){
do {
let data = try PropertyListEncoder().encode(myType)
UserDefaults.standard.set(data, forKey: key)
print("Saved for Key:", key)
} catch {
print("Save Failed")
}
}
private func loadUserDefaults<T: Codable>(withKey key: String) -> T? {
guard let data = UserDefaults.standard.object(forKey: key) as? Data else { return nil }
do {
let decoded = try PropertyListDecoder().decode(T.self, from: data)
return decoded
} catch {
print("Decoding failed for key", key)
return nil
}
}
}
If a value gets set to the value it should automatically save, but like I set sometimes it saves the right values but loads the wrong ones...
In my opinion, It return the cache. Because in Apple official documentation, it state
UserDefaults caches the information to avoid having to open the user’s defaults database each time you need a default value
Maybe you can change the flow, when to save the data. In your code show that you call save() 3 times in init().

How to save a struct with NSCoding

How can I save my struct with NSCoding so that it doesn´t change even if the user
closes the app? I would appreciate it if you could also show me how to implement the missing code correctly.
UPDATE with two new functions below:
Here is my code:
struct RandomItems: Codable
{
var items : [String]
var seen = 0
init(items:[String], seen: Int)
{
self.items = items
self.seen = seen
}
init(_ items:[String])
{ self.init(items: items, seen: 0) }
mutating func next() -> String
{
let index = Int(arc4random_uniform(UInt32(items.count - seen)))
let item = items.remove(at:index)
items.append(item)
seen = (seen + 1) % items.count
return item
}
func toPropertyList() -> [String: Any] {
return [
"items": items,
"seen": seen
]
}
}
override func viewWillDisappear(_ animated: Bool) {
UserDefaults.standard.set(try? PropertyListEncoder().encode(quotes), forKey:"quote2")
}
override func viewDidAppear(_ animated: Bool) {
if let data = UserDefaults.standard.value(forKey:"quote2") as? Data {
let quote3 = try? PropertyListDecoder().decode(Array<RandomItems>.self, from: data)
}
}
}
extension QuotesViewController.RandomItems {
init?(propertyList: [String: Any]) {
return nil
}
}
How can I make sure the whole Array is covered here?
For structs you should be using the new Codable protocol. It is available since swift 4 and is highly recommended.
struct RandomItems: Codable
{
var items: [String]
var seen = 0
}
extension RandomItems {
init?(propertyList: [String: Any]) {
...
}
}
// Example usage
let a = RandomItems(items: ["hello"], seen: 2)
let data: Data = try! JSONEncoder().encode(a)
UserDefaults.standard.set(data, forKey: "MyKey") // Save data to disk
// some time passes
let data2: Data = UserDefaults.standard.data(forKey: "MyKey")! // fetch data from disk
let b = try! JSONDecoder().decode(RandomItems.self, from: data2)
Update
It looks like the Original Poster is nesting the struct inside of another class. Here is another example where there struct is nested.
class QuotesViewController: UIViewController {
struct RandomItems: Codable
{
var items: [String]
var seen = 0
}
}
extension QuotesViewController.RandomItems {
init(_ items:[String])
{ self.items = items }
init?(propertyList: [String: Any]) {
guard let items = propertyList["items"] as? [String] else { return nil }
guard let seen = propertyList["seen"] as? Int else { return nil }
self.items = items
self.seen = seen
}
}
// example usage
let a = QuotesViewController.RandomItems(items: ["hello"], seen: 2)
let data: Data = try! JSONEncoder().encode(a)
UserDefaults.standard.set(data, forKey: "MyKey") // Save data to disk
// some time passes
let data2: Data = UserDefaults.standard.data(forKey: "MyKey")! // fetch data from disk
let b = try! JSONDecoder().decode(QuotesViewController.RandomItems.self, from: data2)