Save Struct to UserDefaults

IosSwiftStructNsuserdefaults

Ios Problem Overview


I have a struct that I want to save to UserDefaults. Here's my struct

struct Song {
    var title: String
    var artist: String
}

var songs: [Song] = [
    Song(title: "Title 1", artist "Artist 1"),
    Song(title: "Title 2", artist "Artist 2"),
    Song(title: "Title 3", artist "Artist 3"),
]

In another ViewController, I have a UIButton that appends to this struct like

@IBAction func likeButtonPressed(_ sender: Any) {   
   songs.append(Song(title: songs[thisSong].title, artist: songs[thisSong].artist))
}

I want it so that whenever the user clicks on that button also, it saves the struct to UserDefaults so that whenever the user quits the app and then opens it agian, it is saved. How would I do this?

Ios Solutions


Solution 1 - Ios

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

Solution 2 - Ios

This is my UserDefaults extension in main thread, to set get Codable object into UserDefaults

// MARK: - UserDefaults extensions

public 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 extensions

public 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 extensions

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

Solution 3 - Ios

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.

Solution 4 - Ios

Here is a modern Swift 5.1 @propertyWrapper, allowing to store any Codable object in form of a human readable JSON string:

@propertyWrapper struct UserDefaultEncoded<T: Codable> {
    let key: String
    let defaultValue: T

    init(key: String, default: T) {
        self.key = key
        defaultValue = `default`
    }

    var wrappedValue: T {
        get {
            guard let jsonString = UserDefaults.standard.string(forKey: key) else {
                return defaultValue
            }
            guard let jsonData = jsonString.data(using: .utf8) else {
                return defaultValue
            }
            guard let value = try? JSONDecoder().decode(T.self, from: jsonData) else {
                return defaultValue
            }
            return value
        }
        set {
            let encoder = JSONEncoder()
            encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
            guard let jsonData = try? encoder.encode(newValue) else { return }
            let jsonString = String(bytes: jsonData, encoding: .utf8)
            UserDefaults.standard.set(jsonString, forKey: key)
        }
    }
}

Usage:

extension Song: Codable {}

@UserDefaultEncoded(key: "songs", default: [])
var songs: [Song]

func addSong(_ song: Song) {
    // This will automatically store new `songs` value 
    // to UserDefaults
    songs.append(song)
}

Solution 5 - Ios

From here:

> A default object must be a property list—that is, an instance of (or for collections, a combination of instances of): NSData , NSString , NSNumber , NSDate , NSArray , or NSDictionary . If you want to store any other type of object, you should typically archive it to create an instance of NSData.

You need to use NSKeydArchiver. Documentation can be found here and examples here and here.

Solution 6 - Ios

If you are just trying to save this array of songs in UserDefaults and nothing fancy use this:-

//stores the array to defaults
UserDefaults.standard.setValue(value: songs, forKey: "yourKey")

//retrieving the array

UserDefaults.standard.object(forKey: "yourKey") as! [Song]
//Make sure to typecast this as an array of Song

If you are storing a heavy array, I suggest you to go with NSCoding protocol or the Codable Protocol in swift 4

Example of coding protocol:-

 struct Song {
        var title: String
        var artist: String
    }
    
    class customClass: NSObject, NSCoding { //conform to nsobject and nscoding
    
    var songs: [Song] = [
        Song(title: "Title 1", artist "Artist 1"),
        Song(title: "Title 2", artist "Artist 2"),
        Song(title: "Title 3", artist "Artist 3"),
    ]
    
    override init(arr: [Song])
    self.songs = arr
    }
    
    required convenience init(coder aDecoder: NSCoder) {
    //decoding your array
    let songs = aDecoder.decodeObject(forKey: "yourKey") as! [Song]
    
    self.init(are: songs)
    }
    
    func encode(with aCoder: NSCoder) {
    //encoding
    aCoder.encode(songs, forKey: "yourKey")
    }

}

Solution 7 - Ios

I'd imagine that it should be quite common to represent a user's settings as an observable object. So, here's an example of keeping observable data synchronised with user defaults and updated for xCode 11.4. This can be used in the context of environment objects also.

import SwiftUI

final class UserData: ObservableObject {

    @Published var selectedAddress: String? {
        willSet {
            UserDefaults.standard.set(newValue, forKey: Keys.selectedAddressKey)
        }
    }
    
    init() {
        selectedAddress = UserDefaults.standard.string(forKey: Keys.selectedAddressKey)
    }
    
    private struct Keys {
        static let selectedAddressKey = "SelectedAddress"
    }
}

Solution 8 - Ios

> Swift 5

If you want need to save struct in UserDefault using only on data format.

Smaple struct

struct StudentData:Codable{
          
          var id: Int?
          var name: String?
          var createdDate: String?
    
      // for decode the  value
      init(from decoder: Decoder) throws {
        let values = try? decoder.container(keyedBy: codingKeys.self)
        id = try? values?.decodeIfPresent(Int.self, forKey: .id)
        name = try? values?.decodeIfPresent(String.self, forKey: .name)
        createdDate = try? values?.decodeIfPresent(String.self, forKey: .createdDate)
      }
      
      // for encode the  value
      func encode(to encoder: Encoder) throws {
        var values = encoder.container(keyedBy: codingKeys.self)
        try? values.encodeIfPresent(id, forKey: .id)
        try? values.encodeIfPresent(name, forKey: .name)
        try? values.encodeIfPresent(createdDate, forKey: .createdDate)
      }
    }

There are two types to convert as data

  1. Codable (Encodable and Decodable).
  2. PropertyListEncoder and PropertyListDecoder

First we using the Codable (Encodable and Decodable) to save the struct

Example for save value

  let value = StudentData(id: 1, name: "Abishek", createdDate: "2020-02-11T11:23:02.3332Z")
  guard let data = try? JSONEncoder().encode(value) else {
    fatalError("unable encode as data")
  }
  UserDefaults.standard.set(data, forKey: "Top_student_record")

Retrieve value

guard let data = UserDefaults.standard.data(forKey: "Top_student_record") else {
  // write your code as per your requirement
  return
}
guard let value = try? JSONDecoder().decode(StudentData.self, from: data) else {
  fatalError("unable to decode this data")
}
print(value)

Now we using the PropertyListEncoder and PropertyListDecoder to save the struct

Example for save value

  let value = StudentData(id: 1, name: "Abishek", createdDate: "2020-02-11T11:23:02.3332Z")
  guard let data = try? PropertyListEncoder().encode(value) else {
    fatalError("unable encode as data")
  }
  UserDefaults.standard.set(data, forKey: "Top_student_record")

Retrieve value

  guard let data = UserDefaults.standard.data(forKey: "Top_student_record") else {
    // write your code as per your requirement
    return
  }
  guard let value = try? PropertyListDecoder().decode(StudentData.self, from: data) else {
    fatalError("unable to decode this data")
  }
  print(value)

In your convenience you can use the any type to save the struct in userDefault.

Solution 9 - Ios

Here is a simpler solution

@propertyWrapper
struct CodableUserDefault<Value: Codable> {
    let key: String
    let defaultValue: Value
    private let container: UserDefaults = .standard

    var wrappedValue: Value {
        get {
            guard let data = container.data(forKey: key), let object = try? JSONDecoder().decode(Value.self, from: data) else {
                return defaultValue
            }
            
            return object
        }
        set {
            container.set(try? JSONEncoder().encode(newValue), forKey: key)
        }
    }
}

Usage

enum ACodableEnum: String, Codable {
   case first
   case second
}

class SomeController {

   @CodableUserDefault<ACodableEnum>(key: "key", defaultValue: .first)
    private var aCodableEnum: ACodableEnum

}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJacob CavinView Question on Stackoverflow
Solution 1 - IosmattView Answer on Stackoverflow
Solution 2 - IosYannStephView Answer on Stackoverflow
Solution 3 - IosvadianView Answer on Stackoverflow
Solution 4 - IoskelinView Answer on Stackoverflow
Solution 5 - IosVladView Answer on Stackoverflow
Solution 6 - IosDark InnocenceView Answer on Stackoverflow
Solution 7 - IosChristopher HuntView Answer on Stackoverflow
Solution 8 - IosAbishek ThangarajView Answer on Stackoverflow
Solution 9 - IosaryaxtView Answer on Stackoverflow