1

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

Igor Voitenko
  • 305
  • 3
  • 14
  • Unrelated to your issue but using `NSHomeDirectory().appending("/Documents/favCreature.bin")` to build your path is wrong. Do a search on `documentDirectory` to find many proper examples. – rmaddy Mar 26 '17 at 20:26
  • Thanks for that! – Igor Voitenko Mar 26 '17 at 20:29
  • You will need to associate a `rawValue` with your enumeration and encore/decode using that – Paulw11 Mar 26 '17 at 20:29
  • Yes, i read about that, but i don't understand exactly how to do that – Igor Voitenko Mar 26 '17 at 20:33
  • Thank's for help, it's not a duplicate, at least it's swift 3 - not swift 1.0, and there is no answer for this question – Igor Voitenko Mar 26 '17 at 20:42
  • 1
    @Paulw11, this question should not be marked as a duplicate which you linked. The enum `Creature` has a case with associated value, so it is not easy to give it a `rawValue` as shown in the link. – OOPer Mar 26 '17 at 20:59
  • You just need to apply the raw value technique twice. Once to save the creature type and another to save the unicorn colour in the event that the creature is a unicorn. – Paulw11 Mar 26 '17 at 21:05
  • @Paulw11, have you really write the code by yourself? It will be far from just _twice_ of the link. – OOPer Mar 26 '17 at 21:21
  • You really have to write the code. enums are native Swift and NSCoding comes from the ObjectiveC world, is it doesn't have native support for enum. You will need to extract the raw value for the creature and save it in a key. In the case that the creature is a unicorn, extract the associated value of the colour and store that in a key. For unarchiving do the same, restore the creature type. If it is a unicorn then get the colour key and then create the creature. – Paulw11 Mar 26 '17 at 21:28
  • The question, as asked, is a duplicate. If the question is "how do you archive a enum with an associated value", then that is a different question – Paulw11 Mar 26 '17 at 21:29
  • @Paulw11, you should show it by code as an answer, rather than writing a long comment. – OOPer Mar 26 '17 at 21:33
  • So that is the problem that i don't know how to extract rawValue from the creature, it's not just creature.rawValue or something like that – Igor Voitenko Mar 26 '17 at 21:36
  • @IgorVoitenko, you'd better edit your question and update the title as suggested by Paulw11. I don't understand why he insists on marking this as a duplicate, but the linked article can be a very little help, and you may need an answer for your own issue. – OOPer Mar 26 '17 at 21:41
  • @OOPer Thank's for advice – Igor Voitenko Mar 26 '17 at 21:45
  • @Paulw11 Question edited and i hope it's not a duplicate anymore – Igor Voitenko Mar 26 '17 at 21:57

3 Answers3

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

Paulw11
  • 108,386
  • 14
  • 159
  • 186
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.

Community
  • 1
  • 1
OOPer
  • 47,149
  • 6
  • 107
  • 142
1

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

enter image description here

Price Ringo
  • 3,424
  • 1
  • 19
  • 33