Search code examples
swiftuiuserdefaultsobservableobject

UserDefaults, ObservableObject and SwiftUI


I'm a little confused on how ObservableObject works with UserDefaults and could use some assistance.

I have a data struct that I need to save when a Background Fetch task runs. When the user resumes the app, I want the new data to be displayed.

As this is a struct, I encode it and save as an object, and decode it when reading it. This all works fine.

I created a DataManager ObservableObject class that publishes the data, so that SwiftUI can display it.

The problem is when it runs the background task I'm saving the object to UserDefaults. But in my SwiftUI view, it doesn't pickup the new data when the app resumes. If I kill the app and re-launch it, the new data shows up. How can I get the new data to display on resume?

// save array in background task

let encoder = JSONEncoder()
if let encoded = try? encoder.encode(input) {
    UserDefaults.standard.set(encoded, forKey: "myData")
}

// ObservableObject

class DataManager: ObservableObject {
    @Published var myData: [MyData] {
        didSet {
            UserDefaults.standard.myData = myData
            objectWillChange.send()
        }

    init() {
        self.myData = UserDefaults.standard.myData
    }
}

// Extension

extension UserDefaults {
//    @objc dynamic var myDate: [MyData] { // Doesn't work with @objc ? but I think this is needed?
dynamic var myData: [MyData] {
    
    get {
        let decoder = JSONDecoder()
        
        if let savedArray = object(forKey: "myData") as? Data {
            if let loadedArray = try? decoder.decode([MyData].self, from: savedArray) {
                return loadedArray
            }
            else {
                return [MyData]()
            }
        }
        else {
            return [MyData]()
        }
    }
    
    set {
        let encoder = JSONEncoder()
        if let encoded = try? encoder.encode(newValue) {
            set(encoded, forKey: "myData")
        }
    }
}

// SwiftUI View

struct MySwiftUI: View {
    @EnvironmentObject var dataManager: DataManager

    var body: some View {
    ForEach(dataManager.myData, id: \.number) { item in
    // Text(item.number)
    }
}

Solution

  • If MyData is Codable you can make the array compatible with RawRepresentable<String>

    //Allows all Codable Arrays to be saved using AppStorage
    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
        }
    }
    

    An then just use @AppStorage, it work straight inside the View or you can put it in the ObservableObject

    class DataManager: ObservableObject {
        @AppStorage("myData") var myData: [MyData] = []
    }
    
    struct MySwiftUI: View {
        @AppStorage("myData") var myData: [MyData] = []
    
        // Your code here.
    }
    

    @AppStorage conforms to DynamicProperty so it has the capability of telling the body to reload when it is needed.