Save Struct to UserDefaults
In Swift 4 this is pretty much trivial. Make your struct codable simply by marking it as adopting the Codable protocol:
struct Song:Codable { var title: String var artist: String}
Now let's start with some data:
var songs: [Song] = [ Song(title: "Title 1", artist: "Artist 1"), Song(title: "Title 2", artist: "Artist 2"), Song(title: "Title 3", artist: "Artist 3"),]
Here's how to get that into UserDefaults:
UserDefaults.standard.set(try? PropertyListEncoder().encode(songs), forKey:"songs")
And here's how to get it back out again later:
if let data = UserDefaults.standard.value(forKey:"songs") as? Data { let songs2 = try? PropertyListDecoder().decode(Array<Song>.self, from: data)}
This is my UserDefaults extension in main thread, to set get Codable object into UserDefaults
// MARK: - UserDefaults extensionspublic extension UserDefaults { /// Set Codable object into UserDefaults /// /// - Parameters: /// - object: Codable Object /// - forKey: Key string /// - Throws: UserDefaults Error public func set<T: Codable>(object: T, forKey: String) throws { let jsonData = try JSONEncoder().encode(object) set(jsonData, forKey: forKey) } /// Get Codable object into UserDefaults /// /// - Parameters: /// - object: Codable Object /// - forKey: Key string /// - Throws: UserDefaults Error public func get<T: Codable>(objectType: T.Type, forKey: String) throws -> T? { guard let result = value(forKey: forKey) as? Data else { return nil } return try JSONDecoder().decode(objectType, from: result) }}
Update This is my UserDefaults extension in background, to set get Codable object into UserDefaults
// MARK: - JSONDecoder extensionspublic extension JSONDecoder { /// Decode an object, decoded from a JSON object. /// /// - Parameter data: JSON object Data /// - Returns: Decodable object public func decode<T: Decodable>(from data: Data?) -> T? { guard let data = data else { return nil } return try? self.decode(T.self, from: data) } /// Decode an object in background thread, decoded from a JSON object. /// /// - Parameters: /// - data: JSON object Data /// - onDecode: Decodable object public func decodeInBackground<T: Decodable>(from data: Data?, onDecode: @escaping (T?) -> Void) { DispatchQueue.global().async { let decoded: T? = self.decode(from: data) DispatchQueue.main.async { onDecode(decoded) } } }}// MARK: - JSONEncoder extensions public extension JSONEncoder { /// Encodable an object /// /// - Parameter value: Encodable Object /// - Returns: Data encode or nil public func encode<T: Encodable>(from value: T?) -> Data? { guard let value = value else { return nil } return try? self.encode(value) } /// Encodable an object in background thread /// /// - Parameters: /// - encodableObject: Encodable Object /// - onEncode: Data encode or nil public func encodeInBackground<T: Encodable>(from encodableObject: T?, onEncode: @escaping (Data?) -> Void) { DispatchQueue.global().async { let encode = self.encode(from: encodableObject) DispatchQueue.main.async { onEncode(encode) } } }} // MARK: - NSUserDefaults extensionspublic extension UserDefaults { /// Set Encodable object in UserDefaults /// /// - Parameters: /// - type: Encodable object type /// - key: UserDefaults key /// - Throws: An error if any value throws an error during encoding. public func set<T: Encodable>(object type: T, for key: String, onEncode: @escaping (Bool) -> Void) throws { JSONEncoder().encodeInBackground(from: type) { [weak self] (data) in guard let data = data, let `self` = self else { onEncode(false) return } self.set(data, forKey: key) onEncode(true) } } /// Get Decodable object in UserDefaults /// /// - Parameters: /// - objectType: Decodable object type /// - forKey: UserDefaults key /// - onDecode: Codable object public func get<T: Decodable>(object type: T.Type, for key: String, onDecode: @escaping (T?) -> Void) { let data = value(forKey: key) as? Data JSONDecoder().decodeInBackground(from: data, onDecode: onDecode) }}
If the struct contains only property list compliant properties I recommend to add a property propertyListRepresentation
and a corresponding init
method
struct Song { var title: String var artist: String init(title : String, artist : String) { self.title = title self.artist = artist } init?(dictionary : [String:String]) { guard let title = dictionary["title"], let artist = dictionary["artist"] else { return nil } self.init(title: title, artist: artist) } var propertyListRepresentation : [String:String] { return ["title" : title, "artist" : artist] }}
To save an array of songs to UserDefaults
write
let propertylistSongs = songs.map{ $0.propertyListRepresentation }UserDefaults.standard.set(propertylistSongs, forKey: "songs")
To read the array
if let propertylistSongs = UserDefaults.standard.array(forKey: "songs") as? [[String:String]] { songs = propertylistSongs.flatMap{ Song(dictionary: $0) }}
If title
and artist
will never be mutated consider to declare the properties as constants (let
) .
This answer was written while Swift 4 was in beta status. Meanwhile conforming to Codable
is the better solution.