5

I'm having a heck of a time trying to figure out how to store an array of my custom struct in UserDefaults.

Here is my code:

struct DomainSchema: Codable {
    var domain: String
    var schema: String
}

var domainSchemas: [DomainSchema] {
    get {
        if UserDefaults.standard.object(forKey: "domainSchemas") != nil {
            let data = UserDefaults.standard.value(forKey: "domainSchemas") as! Data
            let domainSchema = try? PropertyListDecoder().decode(DomainSchema.self, from: data)
            
            return domainSchema!
        }
        
        return nil
    }
    
    set {
        UserDefaults.standard.set(try? PropertyListEncoder().encode(newValue), forKey: "domainSchemas")
    }
}

struct SettingsView: View {
    var body: some View {
        VStack {
            ForEach(domainSchemas, id: \.domain) { domainSchema in
                HStack {
                    Text(domainSchema.domain)
                    Text(domainSchema.schema)
                }
            }
            
            // clear history button
        }
        .onAppear {
            if (domainSchemas.isEmpty) {
                domainSchemas.append(DomainSchema(domain: "reddit.com", schema: "apollo://"))
            }
        }
    }
}

It is giving me these errors:

Cannot convert return expression of type 'DomainSchema' to return type '[DomainSchema]'

'nil' is incompatible with return type '[DomainSchema]'

I'm not really sure how to get an array of the objects instead of just a single object, or how to resolve the nil incompatibility error...

5
  • UserDefaults it is not meant to store your App data. Just use JSONEncoder to encode your collection and write the resulting data to disk. You can save it to your application support directory. Commented Aug 12, 2020 at 0:36
  • Note that you should not use value(forKey:) method. UserDefaults has a specific method to retrieve Data called data(forKey:) Commented Aug 12, 2020 at 0:38
  • OK that makes sense. Should I use core data for this or just write to disk? Commented Aug 12, 2020 at 0:39
  • No need to use Core Data if you have never used it. It all depends on the complexity of your App Data. Commented Aug 12, 2020 at 0:40
  • I have used Core Data before. I only really have 2 sets of data I need to store. 1 is a simple array of strings, the other is this array of what I'm trying to do with the DomainSchema object. Commented Aug 12, 2020 at 0:42

2 Answers 2

5

If you really want to persist your data using UserDefaults the easiest way would be to use a class and conform it to NSCoding. Regarding your global var domainSchemas I would recommend using a singleton or extend UserDefaults and create a computed property for it there:


class DomainSchema: NSObject, NSCoding {
    var domain: String
    var schema: String
    init(domain: String, schema: String) {
        self.domain = domain
        self.schema = schema
    }
    required init(coder decoder: NSCoder) {
        self.domain = decoder.decodeObject(forKey: "domain") as? String ?? ""
        self.schema = decoder.decodeObject(forKey: "schema") as? String ?? ""
    }
    func encode(with coder: NSCoder) {
        coder.encode(domain, forKey: "domain")
        coder.encode(schema, forKey: "schema")
    }
}

extension UserDefaults {
    var domainSchemas: [DomainSchema] {
        get {
            guard let data = UserDefaults.standard.data(forKey: "domainSchemas") else { return [] }
            return (try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data)) as? [DomainSchema] ?? []
        }
        set {
            UserDefaults.standard.set(try? NSKeyedArchiver.archivedData(withRootObject: newValue, requiringSecureCoding: false), forKey: "domainSchemas")
        }
    }
}

Usage:

UserDefaults.standard.domainSchemas = [.init(domain: "a", schema: "b"), .init(domain: "c", schema: "d")]

UserDefaults.standard.domainSchemas  // [{NSObject, domain "a", schema "b"}, {NSObject, domain "c", schema "d"}]


If you prefer the Codable approach persisting the Data using UserDefaults as well:


struct DomainSchema: Codable {
    var domain: String
    var schema: String
    init(domain: String, schema: String) {
        self.domain = domain
        self.schema = schema
    }
}

extension UserDefaults {
    var domainSchemas: [DomainSchema] {
        get {
            guard let data = UserDefaults.standard.data(forKey: "domainSchemas") else { return [] }
            return (try? PropertyListDecoder().decode([DomainSchema].self, from: data)) ?? []
        }
        set {
            UserDefaults.standard.set(try? PropertyListEncoder().encode(newValue), forKey: "domainSchemas")
        }
    }
}

Usage:

UserDefaults.standard.domainSchemas = [.init(domain: "a", schema: "b"), .init(domain: "c", schema: "d")]

UserDefaults.standard.domainSchemas  // [{domain "a", schema "b"}, {domain "c", schema "d"}]

I think the best option would be to do not use UserDefaults, create a singleton "shared instance", declare a domainSchemas property there and save your json Data inside a subdirectory of you application support directory:

extension URL {
    static var domainSchemas: URL {
        let applicationSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
        let bundleID = Bundle.main.bundleIdentifier ?? "company name"
        let subDirectory = applicationSupport.appendingPathComponent(bundleID, isDirectory: true)
        try? FileManager.default.createDirectory(at: subDirectory, withIntermediateDirectories: true, attributes: nil)
        return subDirectory.appendingPathComponent("domainSchemas.json")
    }
}

class Shared {
    static let instance = Shared()
    private init() { }
    var domainSchemas: [DomainSchema] {
        get {
            guard let data = try? Data(contentsOf: .domainSchemas) else { return [] }
            return (try? JSONDecoder().decode([DomainSchema].self, from: data)) ?? []
        }
        set {
            try? JSONEncoder().encode(newValue).write(to: .domainSchemas)
        }
    }
}

Usage:

Shared.instance.domainSchemas = [.init(domain: "a", schema: "b"), .init(domain: "c", schema: "d")]

Shared.instance.domainSchemas  // [{domain "a", schema "b"}, {domain "c", schema "d"}]
Sign up to request clarification or add additional context in comments.

Comments

1

You don't need to use NSKeyedArchiver to save custom objects into UserDefaults Because you have to change your struct into a class. There is an easier solution and That's JSONDecoder and JSONEncoder. Whenever you want to save a custom object into UserDefaults first convert it into Data by using JSONEncoder and when you want to retrieve an object from Userdefaults you do it by using JSONDecoder. Along with that I highly recommend you to write a separate class or struct to manage your data so that being said you can do:

 struct DomainSchema: Codable {
        var domain: String
        var schema: String
 }

 struct PersistenceMangaer{

        static let defaults = UserDefaults.standard
        private init(){}
        
        // save Data method
        static func saveDomainSchema(domainSchema: [DomainSchema]){
          do{
             let encoder = JSONEncoder()
             let domainsSchema = try encoder.encode(domainSchema)
             defaults.setValue(domainsSchema, forKey: "yourKeyName")
          }catch let err{
             print(err)
          }
       }
       
      //retrieve data method
      static func getDomains() -> [DomainSchema]{
    
             guard let domainSchemaData = defaults.object(forKey: "yourKeyName") as? Data else{return}
             do{
                 let decoder = JSONDecoder()
                 let domainsSchema = try decoder.decode([DomainSchema].self, from: domainSchemaData)
                 return domainsSchema
             }catch let err{
                 return([])
           }
        }
 }

Usage:

let domains = PersistenceMangaer.standard.getDomains()
PersistenceMangaer.standard.saveDomainSchema(domainsTosave) 

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.