How can I encode (with NSCoding) an enum that has

2019-07-25 15:12发布

问题:

I'm trying to make my class conform to NSCoding, but running into problems because one of its properties is an enum, like this:

enum Direction {
    case north
    case south
}

If enums were codable I could do it like this:

class MyClass: NSObject, NSCoding {
    var direction: Direction!
    required init(coder aDecoder: NSCoder) {
        direction = aDecoder.decodeObject(forKey: "direction") as! Direction
    }
    func encode(with aCoder: NSCoder) {
        aCoder.encode(direction, forKey: "direction")
    }
}

but enums aren't codable so encode() throws an error.

The answers to "How do I encode enum using NSCoder in swift?" suggest encoding the enum's rawValue and then initializing it from rawValue on the other end. But in this case, Direction doesn't have a rawValue!

It does have a hashValue, which seems promising. I can encode its hashValue without a problem, and decode back to an Int on the other end. But there doesn't seem to be a way to initialize an enum from its hashValue, so I can't turn it back into a Direction.

How can I encode and decode a valueless enum?

回答1:

I think adding a raw value to the enum here is the solution with the least code and is the most maintainable. So if you can modify the enum, add a raw value.

Now let's assume you can't modify the enum. You still can do this in a few ways.

The first one, which I think is quite ugly, is to add an extension of the enum and add a static method like this:

static func direction(from rawValue: String) -> Direction {
    switch rawValue {
        case: "north": return .north
        case: "south": return .south
        default: fatalError()
    }
}

To convert Direction to a codeable value, use String(describing:) to convert the enum to a string. To convert a string back to an enum, just use the method above.

The second one, slightly better, but still not as good as just adding a raw value.

You use a dictionary:

let enumValueDict: [String: Direction] = [
    "north": .north, "south": .south
]

To convert Direction to a codeable value, use String(describing:) to convert the enum to a string. To convert a string back to an enum, just access the dictionary.



回答2:

New in Swift 4, an enum is encodable. However, it must have a raw value. You can easily do with no additional code, however:

enum Direction : String, Codable {
    case north
    case south
}

To make your Codable enum work with an NSCoding class MyClass, implement your init(coder:) and encode(with:) like this:

class MyClass: NSObject, NSCoding {
    var direction: Direction!
    required init(coder aDecoder: NSCoder) {
        direction = (aDecoder as! NSKeyedUnarchiver).decodeDecodable(Direction.self, forKey: "direction")
    }
    func encode(with aCoder: NSCoder) {
        try? (aCoder as! NSKeyedArchiver).encodeEncodable(direction, forKey: "direction")
    }
}


回答3:

You can define some keys and store them instead.

fileprivate let kDirectionKeyNorth = 1
fileprivate let kDirectionKeySouth = 2

// ...

required init(coder aDecoder: NSCoder) {
    let key = aDecoder.decodeInteger(forKey: "direction")
    switch key {
        case kDirectionKeyNorth:
    // ...
    }
}

// and vise versa

It's a bit tricky way. You should always look after the library with Direction is part of. And add keys for new directions.



回答4:

One option is to make the enum conform to Codable. This could be through whichever mechanism you want (e.g. use a raw value such as Int or String, or implement the methods).

So lets assume we have the following:

enum Direction { case north, south, east, west }

extension Direction: Codable {
    // etc etc - make it conform
}

final class MyClass: NSObject {
    var direction: Direction!
}

In the required methods for NSCoding, you can then do the following:

extension MyClass: NSCoding {

    func encode(with aCoder: NSCoder) {
        guard let keyedCoder = aCoder as? NSKeyedArchiver else {
            fatalError("Must use Keyed Coding")
        }
        try! keyedCoder.encodeEncodable(direction, forKey: "direction")
    }

    convenience init?(coder aDecoder: NSCoder) {
        self.init()
        guard let keyedDecoder = aDecoder as? NSKeyedUnarchiver else { 
            fatalError("Must use Keyed Coding") 
        }
        direction = keyedDecoder.decodeDecodable(Direction.self, forKey: "direction") ?? .north
    }
}

So, this works, because NSKeyedArchiver and NSKeyedUnarchiver are aware of Codable, but NSCoder is not. However, given that NSArchiver and NSUnarchiver are now deprecated, and essentially, non-keyed archiving is strongly discouraged, so for your project, it is safe to use fatalError() in this manner - assuming that you test your code.



回答5:

With Swift 5, if your enum Direction can have raw values, you can use Codable (Decodable and Encodable protocols) as an alternative to NSCoding:

enum Direction: String, Codable {
    case north, south
}

final class MyClass: Codable {

    let direction: Direction

    init(direction: Direction) {
        self.direction = direction
    }

}

Usage:

import Foundation

let encoder = JSONEncoder()
let myClass = MyClass(direction: .north)

if let data = try? encoder.encode(myClass),
    let jsonString = String(data: data, encoding: .utf8) {
    print(jsonString)
}

/*
 prints:
 {"direction":"north"}
 */
import Foundation

let jsonString = """
{
  "direction" : "south"
}
"""

let jsonData = jsonString.data(using: .utf8)!
let decoder = JSONDecoder()
let myClassInstance = try! decoder.decode(MyClass.self, from: jsonData)
dump(myClassInstance)

/*
 prints:
 ▿ __lldb_expr_319.MyClass #0
   - direction: __lldb_expr_319.Direction.south
 */