How to archive enum with an associated value? - swift

I'm trying to encode an object and i have some troubles. It work's fine with strings, booleans and else, but i don't know how to use it for enum.
I need to encode this:
enum Creature: Equatable {
enum UnicornColor {
case yellow, pink, white
}
case unicorn(UnicornColor)
case crusty
case shark
case dragon
I'm using this code for encode:
func saveFavCreature(creature: Dream.Creature) {
let filename = NSHomeDirectory().appending("/Documents/favCreature.bin")
NSKeyedArchiver.archiveRootObject(creature, toFile: filename)
}
func loadFavCreature() -> Dream.Creature {
let filename = NSHomeDirectory().appending("/Documents/favCreature.bin")
let unarchived = NSKeyedUnarchiver.unarchiveObject(withFile: filename)
return unarchived! as! Dream.Creature
}
Here is required functions (model.favoriteCreature == Dream.Creature)
override func encode(with aCoder: NSCoder) {
aCoder.encode(model.favoriteCreature, forKey: "FavoriteCreature")
aCoder.encode(String(), forKey: "CreatureName")
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
let favoriteCreature = aDecoder.decodeObject(forKey: "FavoriteCreature")
let name = aDecoder.decodeObject(forKey: "CreatureName")
}
It works fine with "name", i think the problem is in aCoder.encode() coz i don't know what type to write there. I get next error when run:
-[_SwiftValue encodeWithCoder:]: unrecognized selector sent to instance
-[NSKeyedArchiver dealloc]: warning: NSKeyedArchiver deallocated without having had -finishEncoding called on it.
I read some advices in comments and can assume that i have no rawValues in enum Creature, i made raw type "String" in that enum:
enum Creature: String, Equatable {
enum UnicornColor {
case yellow, pink, white
}
case unicorn(UnicornColor)
case crusty
case shark
case dragon
Now i have this error:
Enum with raw type cannot have cases with arguments.
Also i read that associated values and raw values can't coexist. Maybe there is some other way to archive enum without raw values?
Hope someone can help me, thank's

You are dealing with a problem that arises because Swift native features don't always play well with Objective-C. NSCoding has its roots in the Objective-C world, and Objective-C doesn't know anything about Swift Enums, so you can't simply archive an Enum.
Normally, you could just encode/decode the enumeration using raw values, but as you found, you can't combine associated types and raw values in a Swift enumeration.
Unfortunately this means that you will need to build your own 'raw' value methods and handle the cases explicitly in the Creature enumeration:
enum Creature {
enum UnicornColor: Int {
case yellow = 0, pink, white
}
case unicorn(UnicornColor)
case crusty
case shark
case dragon
init?(_ creatureType: Int, color: Int? = nil) {
switch creatureType {
case 0:
guard let rawColor = color,
let unicornColor = Creature.UnicornColor(rawValue:rawColor) else {
return nil
}
self = .unicorn(unicornColor)
case 1:
self = .crusty
case 2:
self = .shark
case 3:
self = .dragon
default:
return nil
}
}
func toRawValues() -> (creatureType:Int, unicornColor:Int?) {
switch self {
case .unicorn(let color):
let rawColor = color.rawValue
return(0,rawColor)
case .crusty:
return (1,nil)
case .shark:
return (2,nil)
case .dragon:
return (3,nil)
}
}
}
You can then encode/decode like this:
class SomeClass: NSObject, NSCoding {
var creature: Creature
init(_ creature: Creature) {
self.creature = creature
}
required init?(coder aDecoder: NSCoder) {
let creatureType = aDecoder.decodeInteger(forKey: "creatureType")
let unicornColor = aDecoder.decodeInteger(forKey: "unicornColor")
guard let creature = Creature(creatureType, color: unicornColor) else {
return nil
}
self.creature = creature
super.init()
}
func encode(with aCoder: NSCoder) {
let creatureValues = self.creature.toRawValues()
aCoder.encode(creatureValues.creatureType, forKey: "creatureType")
if let unicornColor = creatureValues.unicornColor {
aCoder.encode(unicornColor, forKey: "unicornColor")
}
}
}
Testing gives:
let a = SomeClass(.unicorn(.pink))
var data = NSMutableData()
let coder = NSKeyedArchiver(forWritingWith: data)
a.encode(with: coder)
coder.finishEncoding()
let decoder = NSKeyedUnarchiver(forReadingWith: data as Data)
if let b = SomeClass(coder: decoder) {
print(b.creature)
}
unicorn(Creature.UnicornColor.pink)
Personally, I would make Creature a class and use inheritance to deal with the variation between unicorns and other types

The main problem for your issue is that you cannot pass Swift enums to encode(_:forKey:).
This article shown by Paulw11 will help you solve this part. If the enum can easily have rawValue, it's not too difficult.
But, as you see, Enum with raw type cannot have cases with arguments.
Simple enums can easily have rawValue like this:
enum UnicornColor: Int {
case yellow, pink, white
}
But enums with associate values, cannot have rawValue in this way. You may need to manage by yourself.
For example, with having inner enum's rawValue as Int :
enum Creature: Equatable {
enum UnicornColor: Int {
case yellow, pink, white
}
case unicorn(UnicornColor)
case crusty
case shark
case dragon
static func == (lhs: Creature, rhs: Creature) -> Bool {
//...
}
}
You can write an extension for Dream.Creature as:
extension Dream.Creature: RawRepresentable {
var rawValue: Int {
switch self {
case .unicorn(let color):
return 0x0001_0000 + color.rawValue
case .crusty:
return 0x0002_0000
case .shark:
return 0x0003_0000
case .dragon:
return 0x0004_0000
}
}
init?(rawValue: Int) {
switch rawValue {
case 0x0001_0000...0x0001_FFFF:
if let color = UnicornColor(rawValue: rawValue & 0xFFFF) {
self = .unicorn(color)
} else {
return nil
}
case 0x0002_0000:
self = .crusty
case 0x0003_0000:
self = .shark
case 0x0004_0000:
self = .dragon
default:
return nil
}
}
}
(In fact, it is not an actual rawValue and you'd better rename it for a more appropriate name.)
With a definition like shown above, you can utilize the code shown in the link above.

To simplify the coding/decoding you could provide an initializer for Creature requiring a Data and a computed property named data. As Creature changes or as new associated values are added, the interface to NSCoding does not change.
class Foo: NSObject, NSCoding {
let creature: Creature
init(with creature: Creature = Creature.crusty) {
self.creature = creature
super.init()
}
required init?(coder aDecoder: NSCoder) {
guard let data = aDecoder.decodeObject(forKey: "creature") as? Data else { return nil }
guard let _creature = Creature(with: data) else { return nil }
self.creature = _creature
super.init()
}
func encode(with aCoder: NSCoder) {
aCoder.encode(creature.data, forKey: "creature")
}
}
A serialization of Creature into and out of Data could be accomplished like this.
enum Creature {
enum UnicornColor {
case yellow, pink, white
}
case unicorn(UnicornColor)
case crusty
case shark
case dragon
enum Index {
static fileprivate let ofEnum = 0 // data[0] holds enum value
static fileprivate let ofUnicornColor = 1 // data[1] holds unicorn color
}
init?(with data: Data) {
switch data[Index.ofEnum] {
case 1:
switch data[Index.ofUnicornColor] {
case 1: self = .unicorn(.yellow)
case 2: self = .unicorn(.pink)
case 3: self = .unicorn(.white)
default:
return nil
}
case 2: self = .crusty
case 3: self = .shark
case 4: self = .dragon
default:
return nil
}
}
var data: Data {
var data = Data(count: 2)
// the initializer above zero fills data, therefore serialize values starting at 1
switch self {
case .unicorn(let color):
data[Index.ofEnum] = 1
switch color {
case .yellow: data[Index.ofUnicornColor] = 1
case .pink: data[Index.ofUnicornColor] = 2
case .white: data[Index.ofUnicornColor] = 3
}
case .crusty: data[Index.ofEnum] = 2
case .shark: data[Index.ofEnum] = 3
case .dragon: data[Index.ofEnum] = 4
}
return data
}
}

Related

Testing for compliance with and casting to RawRepresentable protocol

I have some generic code that allows me to read and write various types to the defaults system, e.g. value getters and setters:
var value : T {
get {
if T.self == Int.self {
return UserDefaults.standard.integer(forKey: storageKey) as! T
} else if T.self == Double.self {
return UserDefaults.standard.double(forKey: storageKey) as! T
} else if T.self == Float.self {
return UserDefaults.standard.float(forKey: storageKey) as! T
} else if T.self == Bool.self {
return UserDefaults.standard.bool(forKey: storageKey) as! T
} else if T.self == String.self {
return UserDefaults.standard.string(forKey: storageKey) as! T
} else {
return UserDefaults.standard.value(forKey: self.storageKey) as! T
}
}
set(value) {
UserDefaults.standard.set(value, forKey: storageKey)
UserDefaults.standard.synchronize()
}
}
Now I want to add my own enum types to this mechanism by making them RawRepresentable<Int>, e.g.
enum Direction : Int, RawRepresentable {
case left = 0
case right = 1
}
Unfortunately, I can neither find the magic incantation to test whether T conforms to the RawRepresentable protocol, nor can I cast T to the RawRepresentable protocol, because no matter what I try, I always end up with a Protocol 'RawRepresentable' can only be used as a generic constraint because it has Self or associated type requirements.
I have tried every where and as incantation until I have started doubting that it can be done at all!?
I'm in Swift 5 and the goal is to create new instance by invoking CustomType(rawValue:) and getting the Int value by calling myValue.rawValue.
As #vadian said, all those type checks can be replaced be a single call to UserDefaults.standard.object() and conditional casting. Also the type of the value property needs to be an optional to handle the case where the property is not set (or not of the correct type):
struct DefaultKey<T> {
let storageKey: String
var value: T? {
get {
return UserDefaults.standard.object(forKey: storageKey) as? T
}
nonmutating set {
UserDefaults.standard.set(newValue, forKey: storageKey)
}
}
}
And then you can define a constrained extension method where you specialize the computed property for the case of RawRepresentable types:
extension DefaultKey where T: RawRepresentable {
var value: T? {
get {
if let rawValue = UserDefaults.standard.object(forKey: storageKey) as? T.RawValue {
return T(rawValue: rawValue)
}
return nil
}
nonmutating set {
UserDefaults.standard.set(newValue?.rawValue, forKey: storageKey)
}
}
}
Example usage:
enum Direction : Int {
case left = 0
case right = 1
}
let key1 = DefaultKey<Int>(storageKey: "foo")
key1.value = 123
let key2 = DefaultKey<Direction>(storageKey: "bar")
key2.value = .right
print(key1.value as Any) // Optional(123)
print(key2.value as Any) // Optional(Direction.right)
Note that this can still crash if used with non-property-list types. To be on the safe side, you would have to restrict the extensions to types which are known to be user defaults storable (integers, floats, strings, ...):
protocol UserDefaultsStorable {}
extension Int: UserDefaultsStorable {}
extension Float: UserDefaultsStorable {}
// ...
struct DefaultKey<T> {
let storageKey: String
}
extension DefaultKey where T: UserDefaultsStorable { .. }
extension DefaultKey where T: RawRepresentable, T.RawValue: UserDefaultsStorable { ... }

Dealing with dynamic value types with Swift and Codeable

So i am trying to decode a JSON via a Codeable model object. One of the properties, say series_id, can either be an integer or a string. If empty, it is null.
How do I handle that using the decodable option in Swift? I currently have:
struct ContentItem: Codable {
let content_id: String?
let series_id: String?
let rank: Int
let score: Float
}
In my main code I do:
do {
let object = try self.decoder.decode(T.self, from: data)
DispatchQueue.main.async {
completionHandler(.success(object))
}
} catch {
...
}
Thanks!
You can use an enum for that. So it could be either a String or an Int or Unknown:
extension ContentItem {
enum SeriesId: Codable {
case text(String)
case number(Int)
case unknown
}
}
Then you can implement as a Decodable:
extension ContentItem.SeriesId {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let text = try? container.decode(String.self) {
self = .text(text)
} else if let number = try? container.decode(Int.self) {
self = .number(number)
} else {
assertionFailure("Unknown SeriesId type")
self = .unknown
}
}
}
Note that you can expand this to manage whatever Empty means (Since it could be a number as well and empty number is unknown).
Also you can expand it to make it encodable too:
extension ContentItem.SeriesId {
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .text(let text): try container.encode(text)
case .number(let number): try container.encode(number)
case .unknown: throw NSError(domain: "Unknown case is not encodable", code: -1, userInfo: nil)
}
}
}
So you only need to change the type of the series_id:
struct ContentItem: Codable {
let content_id: String?
let series_id: SeriesId?
let rank: Int
let score: Float
}

how to use Codable with Class enum

I feel like I'm going way too far for what should really be a simple thing. With the below code, the error that I get is: Cannot invoke 'decode' with an argument list of type '(GameOfLife.Cell, forKey: GameOfLife.Cell.CodingKeys)'
extension GameOfLife {
enum Cell: Equatable, Codable {
case alive
case born
case dying
case dead
var isAlive: Bool {
switch self {
case .alive, .born: return true
case .dying, .dead: return false
}
}
var isDead: Bool {
switch self {
case .alive, .born: return false
case .dying, .dead: return true
}
}
func equalTo(_ rhs: Cell) -> Bool {
switch (self) {
case .alive, .born:
return rhs.isAlive
case .dead, .dying:
return rhs.isDead
}
}
init(_ living: Bool) {
self = living ? .alive : .dead
}
enum CodingKeys: CodingKey {
case alive
case born
case dying
case dead
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
let leftValue = try container.decode(GameOfLife.Cell.alive, forKey: CodingKeys.alive)
self = GameOfLife.Cell.alive
} catch {
let leftValue = try container.decode(GameOfLife.Cell.born, forKey: CodingKeys.born)
self = GameOfLife.Cell.born
} catch {
let leftValue = try container.decode(GameOfLife.Cell.dying, forKey: CodingKeys.dying)
self = GameOfLife.Cell.dying
} catch {
let leftValue = try container.decode(GameOfLife.Cell.dead, forKey: CodingKeys.dead)
self = GameOfLife.Cell.dead
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .alive:
try container.encode("alive", forKey: .alive)
case .born:
try container.encode("born", forKey: .born)
case .dying:
try container.encode("dying", forKey: .dying)
case .dead:
try container.encode("dead", forKey: .dead)
}
}
}
}
The first parameter of decode( is the expected type of the decoded object. As you are encoding the enum cases a String, it's supposed to be String
let leftValue = try container.decode(String.self, forKey: .alive) // The compiler can infer the type `CodingKeys`
However the entire init(from method makes no sense. If an error occurs inside a catch expression it is not going to be caught in the subsequent catch expression.
To encode an enum case as String just declare the raw type of the enum as String and delete the CodingKeys and the init(from and encode(to methods. And if you want to adopt Equatable you have to implement ==
extension GameOfLife : Codable {
enum Cell: String, Equatable, Codable {
case alive, born, dying, dead
var isAlive: Bool {
switch self {
case .alive, .born: return true
case .dying, .dead: return false
}
}
// isDead can be simplified
var isDead: Bool {
return !isAlive
}
static func == (lhs: Cell, rhs: Cell) -> Bool {
switch (lhs.isAlive, rhs.isAlive) {
case (true, true), (false, false): return true
default: return false
}
}
init(_ living: Bool) {
self = living ? .alive : .dead
}
}
}

Encode nested enumeration swift 3

Purpose
I have a User class with a status property and I want store it into NSUserDefault which need to encode the User class first.
User class code updated version
public class User: NSObject, NSCoding {
override init() {}
var status: Status = .unKnow
required convenience public init(coder aDecoder: NSCoder) {
self.init()
self.status = aDecoder.decodeObject(forKey: "UserStatus") as? Status ?? .unKnow
}
public func encode(with aCoder: NSCoder) {
aCoder.encode(self.status, forKey: "UserStatus")
}
public func updatePersistentData() {
let userDefault = UserDefaults.standard
/// stuck at this line
let encodedData: Data = NSKeyedArchiver.archivedData(withRootObject: self)
userDefault.set(encodedData, forKey: "User")
userDefault.synchronize()
}
}
And my nested enumeration
enum Status {
/// is Login status
case isLogin(style: LoginStatus)
/// is register
case isRegister(style: RegisterStatus)
/// is fetch user info
case isGetUserInfo(style: GetUserInfoStatus)
/// nonabove
case unKnow
}
enum LoginStatus: Int {
case a
case b
case c
case d
}
enum RegisterStatus: Int {
case a
case b
case c
case d
}
enum GetUserInfoStatus: Int {
case a
case b
case c
case d
}
I found this, and understand I need to convert enum into raw value. It seems need to use a normal enum with string-style or Int-style...etc.
And I run the code, error message
HJC[2371:403228] -[_SwiftValue encodeWithCoder:]: unrecognized selector sent to instance 0x7f940e6bec30
HJC[2371:403228] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[_SwiftValue encodeWithCoder:]: unrecognized selector sent to instance 0x7f940e6bec30'
I tried to encode and decode the status since it's the only property in the class but you might need to do the same for the other properties if any was found
First i started with giving the State enum a value that i can encode
enum Status {
enum StatusValue {
case isLogin(LoginStatus)
case isRegister(RegisterStatus)
case isGetUserInfo(RegisterStatus)
case unknow
}
}
extension Status.StatusValue {
var value: Int {
switch self {
case .isLogin(let value):
return value.rawValue
case .isRegister(let value):
return value.rawValue
case .isGetUserInfo(let value):
return value.rawValue
case .unknow:
return -1
}
}
}
enum LoginStatus: Int {
case a = 0
case b
case c
case d
}
enum RegisterStatus: Int {
case a = 4
case b
case c
case d
}
enum GetUserInfoStatus: Int {
case a = 8
case b
case c
case d
}
Second I configured the User class to implement NSCoding
public class User: NSObject, NSCoding {
override init() {
status = .unknow
}
init(_ status: Status.StatusValue) {
self.status = status
}
var status: Status.StatusValue
public func encode(with aCoder: NSCoder) {
print(self.status.value)
aCoder.encode(self.status.value, forKey: "status")
}
public required convenience init(coder aDecoder: NSCoder) {
let status = aDecoder.decodeObject(forKey: "status") as? Status.StatusValue ?? .unknow
self.init(status)
}
func save() {
let savedData = NSKeyedArchiver.archivedData(withRootObject: self)
let defaults = UserDefaults.standard
defaults.set(savedData, forKey: "user")
defaults.synchronize()
}
}
Finally I tested the result through
let user1: User = User()
user1.status = .isLogin(LoginStatus.b)
user1.save()
let user2: User
let defaults = UserDefaults.standard
if let saveduser = defaults.object(forKey: "user") as? Data {
user2 = NSKeyedUnarchiver.unarchiveObject(with: saveduser) as! User
print(user2)
}
I would also suggest to read a little about it in here: NSCoding, Workaround for Swift Enum with raw type + case arguments?

Generic of type RawRepresentable is misinterpreted as self it seems

To use NSCoding with Swift's Enum type I made an extension on NSCoder:
extension NSCoder {
func encodeEnum<Enum: RawRepresentable where Enum.RawValue == String>(value: Enum, forKey key: String) {
self.encodeObject(value.rawValue, forKey: key)
}
func decodeEnumForKey<Enum: RawRepresentable where Enum.RawValue == String>(key: String) -> Enum? {
guard let returnValue = self.decodeObjectForKey(key) as? String else { return nil }
return Enum(rawValue: returnValue)
}
}
The encodeEnum method works fine for a String-backed Enum, but when I try to decode the prior encoded Enum like so:
enum MyEnum: String { case Something, Other }
class MyEnumClass: NSObject, NSCoding {
let myEnum: MyEnum
init(myEnum: MyEnum) {
self.myEnum = myEnum
}
required convenience init?(coder aDecoder: NSCoder) {
guard let tmp = aDecoder.decodeEnumForKey("myKey") as? MyEnum else { return nil }
self.init(myEnum: tmp)
}
}
I get an error on aDecoder.decodeEnumForKey("myKey"):
Value of type `NSCoder` has no member `RawValue`
I'm pretty sure it has something to do with the generic and the condition that Enum.RawValue == String. But I do not understand while it's not working, but works for encodeEnum().
The problem is that in
guard let tmp = aDecoder.decodeEnumForKey("myKey") as? MyEnum else { return nil }
the compiler cannot infer the generic placeholder of
func decodeEnumForKey<Enum: ...>(key: String) -> Enum?
to be MyEnum, you have to cast the result to MyEnum? instead:
guard let tmp = aDecoder.decodeEnumForKey("myKey") as MyEnum? else { return nil }
so that the return type is inferred as MyEnum? from the calling context.