How to archive enum with an associated value?

2019-05-30 16:01发布

问题:

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

回答1:

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.



回答2:

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



回答3:

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