Home > Software design >  Prevent lost of data on AppStorage when changing a struct
Prevent lost of data on AppStorage when changing a struct

Time:04-19

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

CodePudding user response:

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
    }
}
  • Related