How to save an enum conforming to Codable protocol to UserDefaults? - swift

Follow up from my previously question:
I managed to make my enum to conform to Codable protocol, implemented init() and encode() and it seems to work.
enum UserState {
case LoggedIn(LoggedInState)
case LoggedOut(LoggedOutState)
}
enum LoggedInState: String {
case playing
case paused
case stopped
}
enum LoggedOutState: String {
case Unregistered
case Registered
}
extension UserState: Codable {
enum CodingKeys: String, CodingKey {
case loggedIn
case loggedOut
}
enum CodingError: Error {
case decoding(String)
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
if let loggedIn = try? values.decode(String.self, forKey: .loggedIn) {
self = .LoggedIn(LoggedInState(rawValue: loggedIn)!)
}
else if let loggedOut = try? values.decode(String.self, forKey: .loggedOut) {
self = .LoggedOut(LoggedOutState(rawValue: loggedOut)!)
}
else {
throw CodingError.decoding("Decoding failed")
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .LoggedIn(value):
try container.encode(value.rawValue, forKey: .loggedIn)
case let .LoggedOut(value):
try container.encode(value.rawValue, forKey: .loggedOut)
}
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let state = UserState.LoggedIn(.playing)
UserDefaults.standard.set(state, forKey: "state")
}
}
My problem is I don't know how to save it to UserDefaults. If I just save it like I do now I get the following error when running the app:
[User Defaults] Attempt to set a non-property-list object Codable.UserState.LoggedIn(Codable.LoggedInState.playing) as an NSUserDefaults/CFPreferences value for key state
2018-01-20 19:06:26.909349+0200 Codable[6291:789687]

From UserDefaults reference:
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.
So you should encode state manually and store it as data:
if let encoded = try? JSONEncoder().encode(state) {
UserDefaults.standard.set(encoded, forKey: "state")
}
Then, to read it back:
guard let data = UserDefaults.standard.data(forKey: "state") else { fatalError() }
if let saved = try? JSONDecoder().decode(UserState.self, from: data) {
...
}

Swift 4+. Enum with a normal case, and a case with associated value.
Enum's code:
enum Enumeration: Codable {
case one
case two(Int)
private enum CodingKeys: String, CodingKey {
case one
case two
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let value = try? container.decode(String.self, forKey: .one), value == "one" {
self = .one
return
}
if let value = try? container.decode(Int.self, forKey: .two) {
self = .two(value)
return
}
throw _DecodingError.couldNotDecode
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .one:
try container.encode("one", forKey: .one)
case .two(let number):
try container.encode(number, forKey: .two)
}
}
}
Write to UserDefaults:
let one1 = Enumeration.two(442)
if let encoded = try? encoder.encode(one1) {
UserDefaults.standard.set(encoded, forKey: "savedEnumValue")
}
Read from UserDefaults:
if let savedData = UserDefaults.standard.object(forKey: "savedEnumValue") as? Data {
if let loadedValue = try? JSONDecoder().decode(Enumeration.self, from: savedData) {
print(loadedValue) // prints: "two(442)"
}
}

Enum should have rawValue to have ability to be saved as Codable object. Your enum cases have associated values, so they cannot have rawValue.
Article with good explanation
My explanation
Lets imagine that you can save your object .LoggedIn(.playing) to UserDefaults. You want to save UserState's .LoggedIn case. There are 2 main questions:
What should be saved to the storage? Your enum case does not have any value, there is nothing to save. You may think that it has associated value, it can be saved to the storage. So the 2nd question.
(imagine that you saved it to storage) After you get saved value from storage, how are you going to determine what the case is it? You may think that it can be determined by the type of associated value, but what if you have lots of cases with the same associated value types? So the compiler does, it does not know what to do in this situation.
Your problem
You are trying to save the enum's case itself, but the saving to the UserDefaults does not convert your object by default. You can try to encode your .LoggedIn case to Data and then save the resulting data to UserDefaults. When you will need to get saved value you will get the data from storage and decode the enum's case from the data.

Related

Swift encoding: multiple .encode strategeis?

I have a model TestModel that encodes data to JSON to send to an API. It looks like this:
// Called by: JSONEncoder().encode(testModelObject)
class TestModel {
enum CodingKeys: String, CodingKey {
case someKey = "some_key"
case otherKey = "other_key"
case thirdKey = "third_key"
case apiID = "id"
// ... lots of other keys
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(someKeyValue, forKey: .someKey)
try container.encode(otherKeyValue, forKey: .otherKey)
try container.encode(thirdKeyValue, forKey: .thirdKey)
// ... lots of other encoded fields
}
}
The above works fine, however sometimes I wish to send a request to a different endpoint that updates just a single attribute. The update is always going to be for the same attribute. At present I'm sending through all data in encode(), which equals a lot of wasted bandwidth.
I'm sure there's an easy way to do this, but docs/google/stackoverflow aren't proving helpful. So: any thoughts on how to create a second encoding strategy along the lines of the below and call it?
func encodeForUpdate(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(apiID, forKey: .apiID)
try container.encode(justOneValueToUpdate, forKey: .someKey)
}
You need to have a single encode(to encoder: Encoder) function but you can solve this by using a specific CodingKey enum for the second strategy
enum SimpleCodingKeys: String, CodingKey {
case thirdKey = "third_key"
case apiID = "id"
}
and then use the userInfo property of JSONEncoder to tell when you want to use this second enum. First we need a key to use
extension TestModel {
static var useSimpleCodingKeys: CodingUserInfoKey {
return CodingUserInfoKey(rawValue: "useSimpleCodingKeys")!
}
}
and then adjust the encoding function
func encode(to encoder: Encoder) throws {
let useSimple = encoder.userInfo[Self.useSimpleCodingKeys] as? Bool ?? false
if useSimple {
var container = encoder.container(keyedBy: SimpleCodingKeys.self)
try container.encode(apiID, forKey: .apiID)
try container.encode(thirdKeyValue, forKey: .thirdKey)
} else {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(someKeyValue, forKey: .someKey)
try container.encode(otherKeyValue, forKey: .otherKey)
try container.encode(thirdKeyValue, forKey: .thirdKey)
...
}
}
And of course set this value in the dictionary when encoding
let encoder = JSONEncoder()
encoder.userInfo[TestModel.useSimpleCodingKeys] = true

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

Codable enum with multiple keys and associated values

I've seen answers on how to make an enum conform to Codable when all cases have an associated value, but I'm not clear on how to mix enums that have cases with and without associated values:
??? How can I use multiple variations of the same key for a given case?
??? How do I encode/decode cases with no associated value?
enum EmployeeClassification : Codable, Equatable {
case aaa
case bbb
case ccc(Int) // (year)
init?(rawValue: String?) {
guard let val = rawValue?.lowercased() else {
return nil
}
switch val {
case "aaa", "a":
self = .aaa
case "bbb":
self = .bbb
case "ccc":
self = .ccc(0)
default: return nil
}
}
// Codable
private enum CodingKeys: String, CodingKey {
case aaa // ??? how can I accept "aaa", "AAA", and "a"?
case bbb
case ccc
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let value = try? container.decode(Int.self, forKey: .ccc) {
self = .ccc(value)
return
}
// ???
// How do I decode the cases with no associated value?
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .ccc(let year):
try container.encode(year, forKey: .ccc)
default:
// ???
// How do I encode cases with no associated value?
}
}
}
Use the assumed raw string values of the init method as (string) value of the enum case
enum EmployeeClassification : Codable, Equatable {
case aaa
case bbb
case ccc(Int) // (year)
init?(rawValue: String?) {
guard let val = rawValue?.lowercased() else {
return nil
}
switch val {
case "aaa", "a":
self = .aaa
case "bbb":
self = .bbb
case "ccc":
self = .ccc(0)
default: return nil
}
}
// Codable
private enum CodingKeys: String, CodingKey { case aaa, bbb, ccc }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let value = try? container.decode(Int.self, forKey: .ccc) {
self = .ccc(value)
} else if let aaaValue = try? container.decode(String.self, forKey: .aaa), ["aaa", "AAA", "a"].contains(aaaValue) {
self = .aaa
} else if let bbbValue = try? container.decode(String.self, forKey: .bbb), bbbValue == "bbb" {
self = .bbb
} else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath, debugDescription: "Data doesn't match"))
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .aaa: try container.encode("aaa", forKey: .aaa)
case .bbb: try container.encode("bbb", forKey: .bbb)
case .ccc(let year): try container.encode(year, forKey: .ccc)
}
}
}
The Decoding Error is quite generic. You can throw more specific errors for each CodingKey
Starting with Swift 5.5, enums with associated values gained the ability of having automatic conformance to Codable. See this swift-evolution proposal for more details about the implementation details.
So, this is enough for your enum:
enum EmployeeClassification : Codable, Equatable {
case aaa
case bbb
case ccc(Int) // (year)
No more CodingKeys, no more init(from:), or encode(to:)
Thanks #vadian for the great answer 🙏🏻
Another way how to implement custom Decodable / Encodable methods for any enum with associated value and (or) empty cases – using approach with the nestedContainer method called from the root container.
That way describes in the Swift Evolution proposal for Swift 5.5 the support for auto-synthesis of Codable conformance to enums with associated values.
All details and the next implementation I got from the proposal you can also take a look: https://github.com/apple/swift-evolution/blob/main/proposals/0295-codable-synthesis-for-enums-with-associated-values.md
I also extend example from the proposal to cover exactly author's question.
enum Command: Codable {
case load(String)
case store(key: String, Int)
case eraseAll
}
The encode(to:) implementation for Encodable would look as follows:
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .load(key):
var nestedContainer = container.nestedUnkeyedContainer(forKey: .load)
try nestedContainer.encode(key)
case let .store(key, value):
var nestedContainer = container.nestedContainer(keyedBy: StoreCodingKeys.self, forKey: .store)
try nestedContainer.encode(key, forKey: .key)
try nestedContainer.encode(value, forKey: .value)
case .eraseAll:
var nestedContainer = container.nestedUnkeyedContainer(forKey: .eraseAll)
try nestedContainer.encodeNil()
}
}
Please pay attention on some modifications: (1) for load and eraseAll cases I use nestedUnkeyedContainer instead suggested in proposal and eraseAll is the new case which I also added without an associated value.
and the init(from:) implementation for Decodable would look as follows:
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if container.allKeys.count != 1 {
let context = DecodingError.Context(
codingPath: container.codingPath,
debugDescription: "Invalid number of keys found, expected one.")
throw DecodingError.typeMismatch(Command.self, context)
}
switch container.allKeys.first.unsafelyUnwrapped {
case .load:
let nestedContainer = try container.nestedUnkeyedContainer(forKey: .load)
self = .load(try nestedContainer.decode(String.self))
case .store:
let nestedContainer = try container.nestedContainer(keyedBy: StoreCodingKeys.self, forKey: .store)
self = .store(
key: try nestedContainer.decode(String.self, forKey: .key),
value: try nestedContainer.decode(Int.self, forKey: .value))
case .eraseAll:
_ = try container.nestedUnkeyedContainer(forKey: .eraseAll)
self = .eraseAll
}
}

How to save custom type in CoreData with Codable Protocol Swift

I am trying to save the custom object of type codable, In which I am able to store Int16 type. But for [Movie] type in Coredata its NSObject, Entity I have an attribute movie is of type Transformable.
Error: No 'decodeIfPresent' candidates produce the expected contextual
result type 'NSObject?'
How can save this custom type Array with Transformable type
class MovieResults: Results, Codable {
required convenience public init(from decoder: Decoder) throws {
guard let codingUserInfoKeyManagedObjectContext = CodingUserInfoKey.context,
let managedObjectContext = (UIApplication.shared.delegate as? AppDelegate)?.persistentContainer.viewContext,
let entity = NSEntityDescription.entity(forEntityName: "Results", in: managedObjectContext) else {
fatalError("Failed to retrieve managed object context")
}
self.init(entity: entity, insertInto: managedObjectContext)
let container = try decoder.container(keyedBy: CodingKeys.self)
self.page = try container.decodeIfPresent(Int16.self, forKey: .page) ?? 0
self.numberOfResults = try container.decodeIfPresent(Int16.self, forKey: .numberOfResults) ?? 0
self.numberOfPages = try container.decodeIfPresent(Int16.self, forKey: .numberOfPages) ?? 0
self.movies = try container.decodeIfPresent([Movie].self, forKey: .movies) ?? nil
}
// MARK: - Encodable
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(page, forKey: .page)
try container.encode(numberOfResults, forKey: .numberOfResults)
try container.encode(numberOfPages, forKey: .numberOfPages)
try container.encode(movies, forKey: .movies)
}
private enum CodingKeys: String, CodingKey {
case page
case numberOfResults = "total_results"
case numberOfPages = "total_pages"
case movies = "results"
}
}
Movie Array is an custom attribute of type Transformable in CoreData
class Movies: Movie, Codable {
public func encode(to encoder: Encoder) throws {
}
required convenience init(from decoder: Decoder) throws {
guard let codingUserInfoKeyManagedObjectContext = CodingUserInfoKey.context,
let managedObjectContext = decoder.userInfo[codingUserInfoKeyManagedObjectContext] as? NSManagedObjectContext,
let entity = NSEntityDescription.entity(forEntityName: "Movie", in: managedObjectContext) else {
fatalError("Failed to decode User")
}
self.init(entity: entity, insertInto: managedObjectContext)
let container = try decoder.container(keyedBy: CodingKeys.self)
self.identifier = try container.decodeIfPresent(Int16.self, forKey: .identifier) ?? 0
self.posterPath = try container.decodeIfPresent(String.self, forKey: .identifier)
self.backdrop = try container.decodeIfPresent(String.self, forKey: .identifier)
self.title = try container.decodeIfPresent(String.self, forKey: .identifier)
self.releaseDate = try container.decodeIfPresent(String.self, forKey: .identifier)
self.rating = try container.decodeIfPresent(Int16.self, forKey: .rating) ?? 0
self.overview = try container.decodeIfPresent(String.self, forKey: .identifier)
}
enum CodingKeys: String, CodingKey {
case identifier
case posterPath = "poster_path"
case backdrop = "backdrop_path"
case title
case releaseDate = "release_date"
case rating = "vote_average"
case overview
}
}
With this, it's working fine.
self.movies = try container.decodeIfPresent([Movies].self, forKey: .movies)! as NSObject
I am Inheriting NSManagedObject Class Is this Correct way. I tried
using the extension but it throws an error for initializers.?
public convenience init(from decoder: Decoder) throws
Initializer requirement 'init(from:)' can only be satisfied by a
'required' initializer in the definition of non-final class
'MovieResult'
I think this could be done in a more simpler way. Here is how you can give a try. Here is MovieResult class [It's good to have the name of the class in singular].
public class MovieResult: Codable {
public var stringVariable: String
public var intVariable: Int
public init(stringVariable: String, intVariable: Int) {
self.stringVariable = stringVariable
self.intVariable = intVariable
}
}
Here you can persist MovieResult in UserDefaults.
public var savedMovieResult: MovieResult? {
get {
let data = UserDefaults.standard.object(forKey: "savedMovieResultKey") as? Data
var movieResult: MovieResult?
if let data = data {
do {
movieResult = try PropertyListDecoder().decode(MovieResult.self, from: data)
} catch {
print(error)
}
}
return movieResult
} set {
do {
try UserDefaults.standard.set(PropertyListEncoder().encode(newValue), forKey: "savedMovieResultKey")
UserDefaults.standard.synchronize()
} catch {
print(error)
}
}
}

How to use Codable protocol for an enum with other enum as associated value (nested enum)

I asked a question yesterday on how to save a nested enum inside UserDefaults.
I am trying to use the Codable protocol for this, but not sure if I am doing it correctly.
Here is again the enum I want to store -> UserState:
enum UserState {
case LoggedIn(LoggedInState)
case LoggedOut(LoggedOutState)
}
enum LoggedInState: String {
case playing
case paused
case stopped
}
enum LoggedOutState: String {
case Unregistered
case Registered
}
Here are the steps I did so far:
Conform to Codable protocol and specify which are the keys we use for encode/decode:
extension UserState: Codable {
enum CodingKeys: String, CodingKey {
case loggedIn
case loggedOut
}
enum CodingError: Error {
case decoding(String)
}
}
Added initializer for decode:
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
if let loggedIn = try? values.decode(String.self, forKey: .loggedIn) {
self = .LoggedIn(LoggedInState(rawValue: loggedIn)!)
}
if let loggedOut = try? values.decode(String.self, forKey: .loggedOut) {
self = .LoggedOut(LoggedOutState(rawValue: loggedOut)!)
}
throw CodingError.decoding("Decoding failed")
}
Added encode method:
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .LoggedIn(value):
try container.encode(value, forKey: .loggedIn) // --> Ambiguous reference to member 'encode(_:forKey:)'
case let .LoggedOut(value):
try container.encode(value, forKey: .loggedOut) // --> Ambiguous reference to member 'encode(_:forKey:)'
}
}
The encode method gives me the above two errors. Not sure right now what I am doing wrong or if I am on the right track.
Any idea what I am doing wrong and what causes these two ambiguous errors ?
The associated value is LoggedInState or LoggedOutState but you have to encode its rawValue (String):
case let .LoggedIn(value):
try container.encode(value.rawValue, forKey: .loggedIn)
case let .LoggedOut(value):
try container.encode(value.rawValue, forKey: .loggedOut)
}
according to the decode method where you're creating the enum cases from the rawValue.