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?
}
Related
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
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
}
}
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()
}
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
}
Apologies for the stupid question, I'm still really new to the Swift language.
Following up on this answer by #matt, I want to combine these two statements into a single var
UserDefaults.standard.set(try? PropertyListEncoder().encode(songs), forKey:"songs")
if let data = UserDefaults.standard.value(forKey:"songs") as? Data {
let songs2 = try? PropertyListDecoder().decode(Array<Song>.self, from: data)
}
I've thought maybe using a var with didSet {} like something along the lines of
var test: Array = UserDefaults.standard. { // ??
didSet {
UserDefaults.standard.set(try? PropertyListEncoder().encode(test), forKey: "songs")
}
}
But I can't think of where to go from here.
Thanks for the help in advance :))
The property should not be a stored property. It should be a computed property, with get and set accessors:
var songsFromUserDefaults: [Song]? {
get {
if let data = UserDefaults.standard.value(forKey:"songs") as? Data {
return try? PropertyListDecoder().decode(Array<Song>.self, from: data)
} else {
return nil
}
}
set {
if let val = newValue {
UserDefaults.standard.set(try? PropertyListEncoder().encode(val), forKey:"songs")
}
}
}
Notice that since the decoding can fail, the getter returns an optional. This forces the setter to accept an optional newValue, and I have decided to only update UserDefaults when the value is not nil. Another design is to use try! when decoding, or return an empty array when the decoding fails. This way the type of the property can be non-optional, and the nil-check in the setter can be removed.
While you can use computed properties like Sweeper suggested (+1), I might consider putting this logic in a property wrapper.
In SwiftUI you can use AppStorage. Or you can roll your own. Here is a simplified example:
#propertyWrapper public struct Saved<Value: Codable> {
private let key: String
public var wrappedValue: Value? {
get {
guard let data = UserDefaults.standard.data(forKey: key) else { return nil }
return (try? JSONDecoder().decode(Value.self, from: data))
}
set {
guard
let value = newValue,
let data = try? JSONEncoder().encode(value)
else {
UserDefaults.standard.removeObject(forKey: key)
return
}
UserDefaults.standard.set(data, forKey: key)
}
}
init(key: String) {
self.key = key
}
}
And then you can do things like:
#Saved(key: "username") var username: String?
Or
#Saved(key: "songs") var songs: [Song]?