84

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?

pkamb
  • 33,281
  • 23
  • 160
  • 191
Jacob Cavin
  • 2,169
  • 3
  • 19
  • 47
  • Check https://stackoverflow.com/questions/28916535/swift-structs-to-nsdata-and-back – adev Jul 03 '17 at 01:11
  • If you are trying for swift 4. There is new protocol 'Codable' which is great for this kinda stuff. For lesser swift version, you have to create dictionary for your struct and manually parse the data – kathayatnk Jul 03 '17 at 02:34

9 Answers9

290

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)
}
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • 4
    getting error that doesn't confirm to protocol codable – Paragon Dec 28 '17 at 22:01
  • 1
    @Paragon: you have to implement the `func encode(to encoder: Encoder)` method in your struct and then do something like `func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(title, forKey: .title) try container.encode(artist, forKey: . artist) }` – PatrickDotStar Apr 14 '18 at 14:50
  • 2
    One thing I found with this is if your struct changes (e.g. you add a new field) and you try to get it form userdefaults you will get nil. So that is one draw back. – Micro Sep 04 '18 at 05:03
  • 1
    @Micro And that's correct behavior. It has nothing to do with this answer! If the type no longer matches the type that was stored in user defaults, it _shouldn't_ be possible to pull it out of user defaults; the old type literally no longer exists. That's just a feature of you developing the app bit by bit; it has nothing to do with the question or answer here. – matt Sep 04 '18 at 06:53
  • 1
    @matt Just pointing this out incase someone uses this as a user object in their app. If it changes, the user will no longer be accessible. Bug? Feature? YOU DECIDE! – Micro Sep 04 '18 at 15:34
  • my struct is not coddle this is decodable how can I save that decodable struct to the userDefaults ? – Saeed Rahmatolahi Oct 13 '18 at 09:54
  • Is there a particular reason behind your decision of going for `PropertyListEncoder` rather than `JSONEncoder`? – Nicholas Allio Jul 04 '19 at 09:49
  • Although with NSCoding you can create custom encode and decode methods that include version data as part of the structure, and write them to gracefully handle updated versions of your data structures with reasonable fallback behavior for changed fields. I guess you could do the same thing with Codable, but you wouldn't be able to use the convenience of "just define a struct and it works" that you get from Codable when all the properties of your struct are themselves Codable. – Duncan C Jul 27 '19 at 01:00
  • @DuncanC It's not as drastic as that. I've introduced updated versions of a Codable struct into an existing app just by making the new properties Optionals. In any case version migration is always tricky regardless of what you use to archive. – matt Jul 27 '19 at 01:14
  • Is there a way to combine this into a single `var`? – Lemon Nov 22 '20 at 06:28
  • This is an extremely helpful answer. I tried JSONEncode and other options but this is the only version that worked. The reason is the inclusion of Array, which the other answers on SO do not have. Kudos on an elegant and working solution. – FontFamily Nov 26 '20 at 04:16
  • Should be clear and specific. This code make errors – Shourob Datta Mar 29 '21 at 12:02
  • @matt wrt your comment about making new properties Optionals. Can you explain more? I went from saving the struct into UserDefaults to saving it as a Plist but seems like both solutions has issues if I were to change the struct in the future. – app4g Oct 24 '21 at 03:07
  • 1
    @app4g When you try to decode an old version of the struct that lacks properties of the new version of the struct, you succeed because those new properties are optionals. – matt Oct 24 '21 at 03:12
  • 1
    @matt Thanks. I think I understand more now. Esp after finding this SO https://stackoverflow.com/questions/61292446/ios-swift-reading-a-persisted-data-model-after-having-altered-properties-in-th hence you're using an "intermediary" of sorts. – app4g Oct 24 '21 at 03:14
  • this helped me, just need to make sure that any custom types used in the struct are codable also. – Luke Price Dec 08 '22 at 07:21
23

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)
    }
}
Isuru
  • 30,617
  • 60
  • 187
  • 303
YanSte
  • 10,661
  • 3
  • 57
  • 53
  • 1
    I used it as `class func getUser() -> User? { UserDefaults.standard.get(object: User.self, for: DefaultKeys.user) { user in return user } return nil }` But it gives me a warning `Expression of type 'User?' is unused` when returning user value – Bhavin Bhadani Jul 16 '19 at 07:44
  • @EICaptainv2.0 Yes because is optional – YanSte Jul 18 '19 at 02:03
  • So, what to do to get rid of the warning. Warning stay even if I wrapped the return value `Expression of type 'User' is unused` – Bhavin Bhadani Jul 18 '19 at 03:23
  • Is there a way to use this with suiteName as shown here? https://stackoverflow.com/questions/45607903/sharing-userdefaults-between-extensions – Lemon Nov 22 '20 at 05:59
  • extension UserDefaults { static let group = UserDefaults(suiteName: "group.x.x") } try! UserDefaults.group?.set(object: c, forKey: "ok") – YanSte Dec 02 '20 at 19:13
  • @EICaptainv2.0 try this public func get(forKey: String) throws -> T? { guard let result = value(forKey: forKey) as? Data else { return nil } return try JSONDecoder().decode(T.self, from: result) } – Kyle C Oct 16 '21 at 23:48
16

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.

vadian
  • 274,689
  • 30
  • 353
  • 361
  • I think set `propertyListRepresentation` to be `[String:Any]` could be better. – a_tuo Jul 03 '17 at 07:11
  • @a_tuo Why? Both types are clearly `String`. The strong type system of Swift encourages the developer to be as type specific as possible. – vadian Jul 03 '17 at 07:14
  • `[String:Any]` could be more universal if sometime you add "var count: Int" in the `Song` or some other type. That doesn't mean it's not safe. – a_tuo Jul 03 '17 at 07:19
  • 1
    @a_tuo If you are going to add different types the compiler will tell you to change the dictionary. Considering cases which currently never occur is bad programming habit and inefficient. – vadian Jul 03 '17 at 07:23
  • I Know. In the current specific case, this does not seem to make sense. But I think we should provide this way of thinking about the possible situation – a_tuo Jul 03 '17 at 07:27
  • this is for when your struct has two items what can I do when this structure has more than 2 items in it ? – Saeed Rahmatolahi Oct 13 '18 at 05:32
  • 1
    You can add as many items as you want but I highly recommend the `Codable` solution. – vadian Oct 13 '18 at 07:24
11

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)
}
kelin
  • 11,323
  • 6
  • 67
  • 104
  • In C# we use `default(T)`, there is no such thing in swift, I guess purpose of ``default`` is to use `default` keyword as parameter (we call verbatim in C# and use `@default`) – MD TAREQ HASSAN Dec 25 '19 at 05:26
  • @HassanTareq, quotes ` mean that `default` here is not a keyword. – kelin Dec 25 '19 at 14:14
  • Can this be modified/extended so that the caller can use a more standard API like `UserDefaults.standard.set(_, forKey:)` rather than the `@UserDefaultEncoded(key: "songs", default: [])`? – pkamb Mar 15 '20 at 09:30
  • @pkamb, read what [property wrappers](https://docs.swift.org/swift-book/LanguageGuide/Properties.html#ID617) is and you'll see that you don't need to modify this. – kelin Mar 15 '20 at 12:48
  • Your solution both **(1)** encodes/decodes the values AND **(2)** saves them to Standard User Defaults. Is there a way to separate the concerns so that property wrapper handles **(1)** but the caller is responsible for **(2)** saving where they want? For example, your solution does not work in App Group User Defaults. I'd like to use an automatic encoder/decode but then use standard Swift API for saving where I want. – pkamb Mar 15 '20 at 19:26
  • @pkamb, don't forget to vote up, because I spending my time on you. In order to use App Groups replace `UserDefaults.standard` with the shared defaults. – kelin Mar 17 '20 at 16:05
  • I honestly do not understand this answer at all. What is happening in the `Usage` section? I just want to write and read data, the "example" seems incomplete to me – user2161301 Jun 11 '21 at 22:36
  • Usage shows how to declare a var with that wrapper. Then you assign value to that war and it will be stored in UserDefaults. @user2161301, nothing complicated really. Read about [Property Wrapper](https://docs.swift.org/swift-book/LanguageGuide/Properties.html), then return and you'll get everything. I'll update Usage anyway. – kelin Jun 12 '21 at 08:11
  • Now with the function I understand everything, good answer! – user2161301 Jun 12 '21 at 17:13
  • @user2161301, thanks! But don't be a consumer. We all developers here, not a payed teachers. It's ok if answer isn't ideal in educational sense, most important it's technically correct. – kelin Jun 13 '21 at 21:30
3

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.

Vlad
  • 245
  • 1
  • 10
3

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

}
Dark Innocence
  • 1,389
  • 9
  • 17
1

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"
    }
}
Christopher Hunt
  • 2,071
  • 1
  • 16
  • 20
0

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.

0

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

}
aryaxt
  • 76,198
  • 92
  • 293
  • 442