How to encode/decode a dictionary with Codable values for storage in UserDefaults? - nsuserdefaults

I am trying to store a dictionary of company names (string) mapped to Company objects (from a struct Company) in iOS UserDefaults. I have created the Company struct and made it conform to Codable. I have one example a friend helped me with in my project where we created a class Account and stored it in UserDefaults by making a Defaults struct (will include example code). I have read in the swift docs that dictionaries conform to Codable and in order to stay Codable, must contain Codable objects. That is why I made struct Company conform to Codable.
I have created a struct for Company that conforms to Codable. I have tried using model code to create a new struct CompanyDefaults to handle the getting and setting of the Company dictionary from/to UserDefaults. I feel I have some beginner misconceptions about what needs to happen and about how it should be implemented (with good design in mind).
The dictionary I wish to store looks like [String:Company]
where company name will be String and a Company object for Company
I used conform to Codable as I did some research and it seemed like a newer method for completing similar tasks.
Company struct
struct Company: Codable {
var name:String?
var initials:String? = nil
var logoURL:URL? = nil
var brandColor:String? = nil // Change to UIColor
enum CodingKeys:String, CodingKey {
case name = "name"
case initials = "initials"
case logoURL = "logoURL"
case brandColor = "brandColor"
}
init(name:String?, initials:String?, logoURL:URL?, brandColor:String?) {
self.name = name
self.initials = initials
self.logoURL = logoURL
self.brandColor = brandColor
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)
initials = try values.decode(String.self, forKey: .initials)
logoURL = try values.decode(URL.self, forKey: .logoURL)
brandColor = try values.decode(String.self, forKey: .brandColor)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(initials, forKey: .initials)
try container.encode(logoURL, forKey: .logoURL)
try container.encode(brandColor, forKey: .brandColor)
}
}
Defaults struct to control storage
struct CompanyDefaults {
static private let companiesKey = "companiesKey"
static var companies: [String:Company] = {
guard let data = UserDefaults.standard.data(forKey: companiesKey) else { return [:] }
let companies = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? [String : Company] ?? [:]
return companies!
}() {
didSet {
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: companies, requiringSecureCoding: false) else {
return
}
UserDefaults.standard.set(data, forKey: companiesKey)
}
}
}
I should be able to reference the stored dictionary throughout my code like CompanyDefaults.companies.count
For reference, a friend helped me do a similar task for an array of Account classes stored in user defaults. The code that works perfectly for that is below. The reason I tried a different way is that I had a different data structure (dictionary) and made the decision to use structs.
class Account: NSObject, NSCoding {
let service: String
var username: String
var password: String
func encode(with aCoder: NSCoder) {
aCoder.encode(service)
aCoder.encode(username)
aCoder.encode(password)
}
required init?(coder aDecoder: NSCoder) {
guard let service = aDecoder.decodeObject() as? String,
var username = aDecoder.decodeObject() as? String,
var password = aDecoder.decodeObject() as? String else {
return nil
}
self.service = service
self.username = username
self.password = password
}
init(service: String, username: String, password: String) {
self.service = service
self.username = username
self.password = password
}
}
struct Defaults {
static private let accountsKey = "accountsKey"
static var accounts: [Account] = {
guard let data = UserDefaults.standard.data(forKey: accountsKey) else { return [] }
let accounts = NSKeyedUnarchiver.unarchiveObject(with: data) as? [Account] ?? []
return accounts
}() {
didSet {
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: accounts, requiringSecureCoding: false) else {
return
}
UserDefaults.standard.set(data, forKey: accountsKey)
}
}
}

You are mixing up NSCoding and Codable. The former requires a subclass of NSObject, the latter can encode the structs and classes directly with JSONEncoder or ProperListEncoder without any Keyedarchiver which also belongs to NSCoding.
Your struct can be reduced to
struct Company: Codable {
var name : String
var initials : String
var logoURL : URL?
var brandColor : String?
}
That's all, the CodingKeys and the other methods are synthesized. I would at least declare name and initials as non-optional.
To read and save the data is pretty straightforward. The corresponding CompanyDefaults struct is
struct CompanyDefaults {
static private let companiesKey = "companiesKey"
static var companies: [String:Company] = {
guard let data = UserDefaults.standard.data(forKey: companiesKey) else { return [:] }
return try? JSONDecoder.decode([String:Company].self, from: data) ?? [:]
}() {
didSet {
guard let data = try? JSONEncoder().encode(companies) else { return }
UserDefaults.standard.set(data, forKey: companiesKey)
}
}
}

Related

'Failed to call designated initializer on NSManagedObject class' NSManagedObject and NSCoding

I'm attempting to:
Load an array of custom objects from JSON file.
Save it in CoreData.
Fetch it using NSFetchRequest.
Note: Each element in the array of custom objects named 'Cube' also contains a nested array of custom objects named 'Algorithm', which also conform to NSCoding (shown in code snippet below).
Steps 1 + 2 seem to work fine. The error occurs when fetching the top level Entities named 'Cube', specifically right after the self.init() is called inside the nested class 'Algorithm'.
Algorithm class:
#objc(Algorithm)
public class Algorithm: NSManagedObject, Decodable, Encodable, NSSecureCoding {
enum CodingKeys: String, CodingKey {
case imageNumber, alg, alternativeAlgs, type, tags, isFavorite, optimalMoves, name }
public static var supportsSecureCoding = true
public func encode(with coder: NSCoder) {
coder.encode(imageNumber, forKey: CodingKeys.imageNumber.rawValue)
coder.encode(optimalMoves, forKey: CodingKeys.optimalMoves.rawValue)
coder.encode(alg, forKey: CodingKeys.alg.rawValue)
coder.encode(name, forKey: CodingKeys.name.rawValue)
coder.encode(type, forKey: CodingKeys.type.rawValue)
coder.encode(isFavorite, forKey: CodingKeys.isFavorite.rawValue)
coder.encode(alternativeAlgs, forKey: CodingKeys.alternativeAlgs.rawValue)
coder.encode(tags, forKey: CodingKeys.tags.rawValue) }
required convenience public init?(coder: NSCoder) {
self.init()
imageNumber = coder.decodeObject(forKey: CodingKeys.imageNumber.rawValue) as? String ?? ""
optimalMoves = coder.decodeObject(forKey: CodingKeys.optimalMoves.rawValue) as? String ?? ""
alg = coder.decodeObject(forKey: CodingKeys.alg.rawValue) as? String ?? ""
name = coder.decodeObject(forKey: CodingKeys.name.rawValue) as? String ?? ""
type = coder.decodeObject(forKey: CodingKeys.type.rawValue) as? String ?? ""
isFavorite = coder.decodeBool(forKey: CodingKeys.isFavorite.rawValue)
alternativeAlgs = coder.decodeObject(forKey: CodingKeys.alternativeAlgs.rawValue) as? [String] ?? []
tags = coder.decodeObject(forKey: CodingKeys.tags.rawValue) as? [String] ?? [] }
required convenience public init(from decoder: Decoder) throws {
guard let context = decoder.userInfo[CodingUserInfoKey.managedObjectContext] as? NSManagedObjectContext else {
throw DecoderConfigurationError.missingManagedObjectContext
}
self.init(context: context)
let container = try decoder.container(keyedBy: CodingKeys.self)
self.imageNumber = try container.decode(String.self, forKey: .imageNumber)
self.optimalMoves = try container.decode(String.self, forKey: .optimalMoves)
self.alg = try container.decode(String.self, forKey: .alg)
self.name = try container.decode(String.self, forKey: .name)
self.type = try container.decode(String.self, forKey: .type)
self.isFavorite = try container.decode(Bool.self, forKey: .isFavorite)
self.alternativeAlgs = try container.decode([String].self, forKey: .alternativeAlgs) as [String]
self.tags = try container.decode([String].self, forKey: .tags) as [String] }
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(imageNumber, forKey: .imageNumber)
try container.encode(optimalMoves, forKey: .optimalMoves)
try container.encode(alg, forKey: .alg)
try container.encode(name, forKey: .name)
try container.encode(type, forKey: .type)
try container.encode(isFavorite, forKey: .isFavorite)
try container.encode(alternativeAlgs, forKey: .alternativeAlgs)
try container.encode(tags, forKey: .tags) }
}
extension Algorithm {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Algorithm> {
return NSFetchRequest<Algorithm>(entityName: "Algorithm")
}
#NSManaged public var alg: String
#NSManaged public var alternativeAlgs: [String]
#NSManaged public var imageNumber: String
#NSManaged public var isFavorite: Bool
#NSManaged public var name: String
#NSManaged public var optimalMoves: String
#NSManaged public var tags: [String]
#NSManaged public var type: String
#NSManaged public var cube: Cube?
}
extension Algorithm : Identifiable {
}
After reading people's comments I am aware I should replace the self.init() with NSManagedObject's designated init, but I haven't found the right way to do so. The app crashes after replacing the self.init() with the following code:
let context = CoreDataManager.shared.container.viewContext
self.init(context: context)
// self.init()
FWI - in the CoreData.xcdatamodeled file inspector the algorithms array is defined as Transformable with a custom transformer - AlgorithmDataTransformer:
#objc(AlgorithmDataTransformer)
public final class AlgorithmDataTransformer: ValueTransformer {
override public func transformedValue(_ value: Any?) -> Any? {
guard let array = value as? [Algorithm] else { return nil }
do {
return try NSKeyedArchiver.archivedData(withRootObject: array, requiringSecureCoding: true)
} catch {
assertionFailure("Failed to transform `Algorithm` to `Data`")
return nil
}
}
override public func reverseTransformedValue(_ value: Any?) -> Any? {
guard let data = value as? NSData else { return nil }
do {
return try NSKeyedUnarchiver.unarchivedArrayOfObjects(ofClass: Algorithm.self, from: data as Data)
} catch {
assertionFailure("Failed to transform `Data` to `Algorithm`")
return nil
}
}
}
extension AlgorithmDataTransformer {
/// The name of the transformer. This is the name used to register the transformer using `ValueTransformer.setValueTrandformer(_"forName:)`.
static let name = NSValueTransformerName(rawValue: String(describing: AlgorithmDataTransformer.self))
/// Registers the value transformer with `ValueTransformer`.
public static func register() {
let transformer = AlgorithmDataTransformer()
ValueTransformer.setValueTransformer(transformer, forName: name)
}
}

Store custom data type in #AppStorage with optional initializer?

I'm trying to store a custom data type into AppStorage. To do so, the model conforms to RawRepresentable (followed this tutorial). It's working fine, but when I initialize the #AppStorage variable, it requires an initial UserModel value. I want to make the variable optional, so it can be nil if the user is signed out. Is this possible?
Within a class / view, I can init like this:
#AppStorage("user_model") private(set) var user: UserModel = UserModel(id: "", name: "", email: "")
But I want to init like this:
#AppStorage("user_model") private(set) var user: UserModel?
Model:
struct UserModel: Codable {
let id: String
let name: String
let email: String
enum CodingKeys: String, CodingKey {
case id
case name
case email
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
do {
id = try String(values.decode(Int.self, forKey: .id))
} catch DecodingError.typeMismatch {
id = try String(values.decode(String.self, forKey: .id))
}
self.name = try values.decode(String.self, forKey: .name)
self.email = try values.decode(String.self, forKey: .email)
}
init(id: String, name: String, email: String) {
self.id = id
self.name = name
self.email = email
}
}
// MARK: RAW REPRESENTABLE
extension UserModel: RawRepresentable {
// RawRepresentable allows a UserModel to be store in AppStorage directly.
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode(UserModel.self, from: data)
else {
return nil
}
self = result
}
var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else {
return "[]"
}
return result
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(name, forKey: .name)
try container.encode(email, forKey: .email)
}
}
The code below works because you added a conformance UserModel: RawRepresentable:
#AppStorage("user_model") private(set) var user: UserModel = UserModel(id: "", name: "", email: "")
You need to do the same for UserModel? if you want the following to work:
#AppStorage("user_model") private(set) var user: UserModel? = nil
Here is a possible solution:
extension Optional: RawRepresentable where Wrapped == UserModel {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode(UserModel.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
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: UserModel.CodingKeys.self)
try container.encode(self?.id, forKey: .id)
try container.encode(self?.name, forKey: .name)
try container.encode(self?.email, forKey: .email)
}
}
Note: I reused the implementation you already had for UserModel: RawRepresentable - it might need some corrections for this case.
Also because you conform Optional: RawRepresentable you need to make
UserModel public as well.
A possible generic approach for any optional Codable:
extension Optional: RawRepresentable where Wrapped: Codable {
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let json = String(data: data, encoding: .utf8)
else {
return "{}"
}
return json
}
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let value = try? JSONDecoder().decode(Self.self, from: data)
else {
return nil
}
self = value
}
}
With that in place, any Codable can now be persisted in app storage:
#AppStorage("user_model") var user: UserModel? = nil

SwiftUI: Encode a struct to be saved in AppStorage

Currently trying to build my first app in swiftUI. The part I thought would be the easiest as become a nightmare... save a struct in AppStorage to be available upon restart of the app
I got two struct to save. The first is for player and I have implemented the RawRepresentable
struct Player: Codable, Identifiable {
let id: Int
let name: String
let gamePlayed: Int
let bestScore: Int
let nbrGameWon: Int
let nbrGameLost: Int
let totalScore: Int?
}
typealias PlayerList = [Player]
extension PlayerList: RawRepresentable {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode(PlayerList.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
}
}
Calling in my view this way:
struct AddPlayerView: View {
#State var name: String = ""
#State var isDisabled: Bool = false
#State var modified: Bool = false
#AppStorage("players") var players: PlayerList = PlayerList()
...
}
The above works, now I also want to save the current game data, I have the following struct:
struct Game: Codable, Identifiable {
var id: Int
var currentPlayerIndexes: Int
var currentRoundIndex: Int?
var dealerIndex: Int?
var maxRounds: Int?
var dealResults: [Int: Array<PlayerRoundSelection>]?
var currentLeaderIds: Array<Int>?
var isGameInProgress: Bool?
}
extension Game: RawRepresentable {
public init?(rawValue: String) {
if rawValue == "" {
// did to fix issue when calling AppStorage, but it is probably a bad idea
self = Game(id:1, currentPlayerIndexes:1)
}
else {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode(Game.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
}
}
As soon as I try to modify the struct, it calls rawValue and the encoding fails with the following:
error: warning: couldn't get required object pointer (substituting NULL): Couldn't load 'self' because its value couldn't be evaluated
error: Execution was interrupted, reason: EXC_BAD_ACCESS (code=2, address=0x7ffee49bbff8).
Here part of the code that access the struct:
struct SelectPlayersView: View {
#AppStorage("currentGame") var currentGame: Game = Game(rawValue: "")!
....
NavigationLink(
destination: SelectModeTypeView(), tag: 2, selection: self.$selection) {
ActionButtonView(text:"Next", disabled: self.$isDisabled, buttonAction: {
var currentPlayers = Array<Int>()
self.players.forEach({ player in
if selectedPlayers.contains(player.id) {
currentPlayers.insert(player.id, at: currentPlayers.count)
}
})
// This used to be a list of indexes, but for testing only using a single index
self.currentGame.currentPlayerIndexes = 6
self.selection = 2
})
...
I found the code to encode here: https://lostmoa.com/blog/SaveCustomCodableTypesInAppStorageOrSceneStorage/
My understanding is that with the self in the encode, it generate an infinite loop hence the bad access.
I have really no knowledge how to properly encode this, any help, links would be appreciated
I had the same problem and I wanted to share my experience here.
I eventually found that apparently you cannot rely on the default Codable protocol implementation when used in combination with RawRepresentable.
So when I did my own Codable implementation, with CodingKeys and all, it worked!
I think your Codable implementation for Game would be something like:
enum CodingKeys: CodingKey {
case currentPlayerIndexes
case currentRoundIndex
// <all the other elements too>
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.currentPlayerIndexes = try container.decode(Int.self, forKey: .currentPlayerIndexes)
self.currentRoundIndex = try container.decode(Int.self, forKey: .currentRoundIndex)
// <and so on>
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(currentPlayerIndexes, forKey: .currentPlayerIndexes)
try container.encode(currentRoundIndex, forKey: .currentRoundIndex)
// <and so on>
}
I then wondered why your Player coding/decoding did work and found that the default coding and decoding of an Array (i.e. the PlayerList, which is [Player]), works fine.

Im trying to save custom data to cloud firestore in swiftUI, Im trying to construct the data i get from snapshot listener

The call document.data(as: ) requires Decodable protocol, it saves the data just fine as the call to addDocument(from:) requires encodable object so I pass my custom class object, yet I don't know why the other way around requires the protocol.
How do I pass the protocol?
Here is my custom class definition :
class PersonalInfo: Codable, ObservableObject {
#Published var name: String
#Published var age: Int
#Published var Sex: String
enum CodingKeys: CodingKey{
case name, age, Sex
}
init(name: String, age:Int, Sex:String) {
self.name = name
self.age = age
self.Sex = Sex
}
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
age = try container.decode(Int.self, forKey: .age)
Sex = try container.decode(String.self, forKey: .Sex)
}
public func encode( to encoder: Encoder) throws{
var container = try encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(age, forKey: .age)
try container.encode(Sex, forKey: .Sex)
}
}
Here is the function I am trying to construct the data after getting set of data:
mutating func loadData(){
let listener = query?.addSnapshotListener{ (snapShot, error ) in
guard let snapshot = snapShot else {
print("Error listening")
return
}
let userCollection = try? snapshot.documents.map{ document -> PersonalInfo in
let decoder = JSONDecoder()
if let user = try document.data(as: PersonalInfo(from: decoder)){
return user
}
else{
fatalError("unable to initialize type")
}
}
}
}
There is no need to create a class for your data structure. A Class: ObservableObject accepts, processes, and passes (published) needed data(values) to your View according to your data structure...
A good news: Firebase supports the Swift Codable Protocol – check this link

Get 'NSInvalidArgumentException' 'Attempt to insert non-property list object when saving class type array to UserDefaults [duplicate]

I have a simple object which conforms to the NSCoding protocol.
import Foundation
class JobCategory: NSObject, NSCoding {
var id: Int
var name: String
var URLString: String
init(id: Int, name: String, URLString: String) {
self.id = id
self.name = name
self.URLString = URLString
}
// MARK: - NSCoding
required init(coder aDecoder: NSCoder) {
id = aDecoder.decodeObject(forKey: "id") as? Int ?? aDecoder.decodeInteger(forKey: "id")
name = aDecoder.decodeObject(forKey: "name") as! String
URLString = aDecoder.decodeObject(forKey: "URLString") as! String
}
func encode(with aCoder: NSCoder) {
aCoder.encode(id, forKey: "id")
aCoder.encode(name, forKey: "name")
aCoder.encode(URLString, forKey: "URLString")
}
}
I'm trying to save an instance of it in UserDefaults but it keeps failing with the following error.
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to insert non-property list object for key jobCategory'
This is the code where I'm saving in UserDefaults.
enum UserDefaultsKeys: String {
case jobCategory
}
class ViewController: UIViewController {
#IBAction func didTapSaveButton(_ sender: UIButton) {
let category = JobCategory(id: 1, name: "Test Category", URLString: "http://www.example-job.com")
let userDefaults = UserDefaults.standard
userDefaults.set(category, forKey: UserDefaultsKeys.jobCategory.rawValue)
userDefaults.synchronize()
}
}
I replaced the enum value to key with a normal string but the same error still occurs. Any idea what's causing this?
You need to create Data instance from your JobCategory model using JSONEncoder and store that Data instance in UserDefaults and later decode using JSONDecoder.
struct JobCategory: Codable {
let id: Int
let name: String
}
// To store in UserDefaults
if let encoded = try? JSONEncoder().encode(category) {
UserDefaults.standard.set(encoded, forKey: UserDefaultsKeys.jobCategory.rawValue)
}
// Retrieve from UserDefaults
if let data = UserDefaults.standard.object(forKey: UserDefaultsKeys.jobCategory.rawValue) as? Data,
let category = try? JSONDecoder().decode(JobCategory.self, from: data) {
print(category.name)
}
Old Answer
You need to create Data instance from your JobCategory instance using archivedData(withRootObject:) and store that Data instance in UserDefaults and later unarchive using unarchiveTopLevelObjectWithData(_:), So try like this.
For Storing data in UserDefaults
let category = JobCategory(id: 1, name: "Test Category", URLString: "http://www.example-job.com")
let encodedData = NSKeyedArchiver.archivedData(withRootObject: category, requiringSecureCoding: false)
let userDefaults = UserDefaults.standard
userDefaults.set(encodedData, forKey: UserDefaultsKeys.jobCategory.rawValue)
For retrieving data from UserDefaults
let decoded = UserDefaults.standard.object(forKey: UserDefaultsKeys.jobCategory.rawValue) as! Data
let decodedTeams = NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(decoded) as! JobCategory
print(decodedTeams.name)
Update Swift 4, Xcode 10
I have written a struct around it for easy access.
//set, get & remove User own profile in cache
struct UserProfileCache {
static let key = "userProfileCache"
static func save(_ value: Profile!) {
UserDefaults.standard.set(try? PropertyListEncoder().encode(value), forKey: key)
}
static func get() -> Profile! {
var userData: Profile!
if let data = UserDefaults.standard.value(forKey: key) as? Data {
userData = try? PropertyListDecoder().decode(Profile.self, from: data)
return userData!
} else {
return userData
}
}
static func remove() {
UserDefaults.standard.removeObject(forKey: key)
}
}
Profile is a Json encoded object.
struct Profile: Codable {
let id: Int!
let firstName: String
let dob: String!
}
Usage:
//save details in user defaults...
UserProfileCache.save(profileDetails)
Hope that helps!!!
Thanks
Swift save Codable object to UserDefault with #propertyWrapper
#propertyWrapper
struct UserDefault<T: Codable> {
let key: String
let defaultValue: T
init(_ key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
if let data = UserDefaults.standard.object(forKey: key) as? Data,
let user = try? JSONDecoder().decode(T.self, from: data) {
return user
}
return defaultValue
}
set {
if let encoded = try? JSONEncoder().encode(newValue) {
UserDefaults.standard.set(encoded, forKey: key)
}
}
}
}
enum GlobalSettings {
#UserDefault("user", defaultValue: User(name:"",pass:"")) static var user: User
}
Example User model confirm Codable
struct User:Codable {
let name:String
let pass:String
}
How to use it
//Set value
GlobalSettings.user = User(name: "Ahmed", pass: "Ahmed")
//GetValue
print(GlobalSettings.user)
Save dictionary Into userdefault
let data = NSKeyedArchiver.archivedData(withRootObject: DictionaryData)
UserDefaults.standard.set(data, forKey: kUserData)
Retrieving the dictionary
let outData = UserDefaults.standard.data(forKey: kUserData)
let dict = NSKeyedUnarchiver.unarchiveObject(with: outData!) as! NSDictionary
Based on Harjot Singh answer. I've used like this:
struct AppData {
static var myObject: MyObject? {
get {
if UserDefaults.standard.object(forKey: "UserLocationKey") != nil {
if let data = UserDefaults.standard.value(forKey: "UserLocationKey") as? Data {
let myObject = try? PropertyListDecoder().decode(MyObject.self, from: data)
return myObject!
}
}
return nil
}
set {
UserDefaults.standard.set(try? PropertyListEncoder().encode(newValue), forKey: "UserLocationKey")
}
}
}
Here's a UserDefaults extension to set and get a Codable object, and keep it human-readable in the plist (User Defaults) if you open it as a plain text file:
extension Encodable {
var asDictionary: [String: Any]? {
guard let data = try? JSONEncoder().encode(self) else { return nil }
return try? JSONSerialization.jsonObject(with: data) as? [String : Any]
}
}
extension Decodable {
init?(dictionary: [String: Any]) {
guard let data = try? JSONSerialization.data(withJSONObject: dictionary) else { return nil }
guard let object = try? JSONDecoder().decode(Self.self, from: data) else { return nil }
self = object
}
}
extension UserDefaults {
func setEncodableAsDictionary<T: Encodable>(_ encodable: T, for key: String) {
self.set(encodable.asDictionary, forKey: key)
}
func getDecodableFromDictionary<T: Decodable>(for key: String) -> T? {
guard let dictionary = self.dictionary(forKey: key) else {
return nil
}
return T(dictionary: dictionary)
}
}
If you want to also support array (of codables) to and from plist array, add the following to the extension:
extension UserDefaults {
func setEncodablesAsArrayOfDictionaries<T: Encodable>(_ encodables: Array<T>, for key: String) {
let arrayOfDictionaries = encodables.map({ $0.asDictionary })
self.set(arrayOfDictionaries, forKey: key)
}
func getDecodablesFromArrayOfDictionaries<T: Decodable>(for key: String) -> [T]? {
guard let arrayOfDictionaries = self.array(forKey: key) as? [[String: Any]] else {
return nil
}
return arrayOfDictionaries.compactMap({ T(dictionary: $0) })
}
}
If you don't care about plist being human-readable, it can be simply saved as Data (will look like random string if opened as plain text):
extension UserDefaults {
func setEncodable<T: Encodable>(_ encodable: T, for key: String) throws {
let data = try PropertyListEncoder().encode(encodable)
self.set(data, forKey: key)
}
func getDecodable<T: Decodable>(for key: String) -> T? {
guard
self.object(forKey: key) != nil,
let data = self.value(forKey: key) as? Data
else {
return nil
}
let obj = try? PropertyListDecoder().decode(T.self, from: data)
return obj
}
}
(With this second approach, you don't need the Encodable and Decodable extensions from the top)