1
votes

I'm struggling to understand class/reference type behavior and how this relates to changes as I try to upgrade and reduce code using Codable in Swift 4.

I have two classes – a SuperClass with all of the data that will be persistent and that I save to UserDefaults (a place name & string with coordinates), and a SubClass that contains additional, temporary info that I don't need (weather data for the SuperClass coordinates).

In Swift 3 I used to save data like this:

func saveUserDefaults() {
    var superClassArray = [SuperClass]()
    // subClassArray is of type [SubClass] and contains more data per element.
   superClassArray = subClassArray
    let superClassData = NSKeyedArchiver.archivedData(withRootObject: superClassArray)
    UserDefaults.standard.set(superClassData, forKey: " superClassData")
}

SuperClass conformed to NSObject & NSCoding It also included the required init decoder & the encode function. It all worked fine.

In trying to switch to Swift 4 & codable I've modified SuperClass to conform to Codable. SuperClass now only has one basic initializer and none of the encoder/decoder stuff from Swift 3. There is no KeyedArchiving happening with this new approach (below). SubClass remains unchanged. Unfortunately I crash on the line where I try? encoder.encode [giving a Thread 1: EXC_BAD_ACCESS (code=1, address=0x10)]. My assumption is that the encoder is getting confused with identical reference types where one is SuperClass and one SubClass (subClassArray[0] === superClassArray[0] is true). I thought this might work:

func saveUserDefaults() {
   var superClassArray = [SuperClass]()
    superClassArray = subClassArray
    // assumption was that the subclass would only contain parts of the superclass & wouldn't produce an error when being encoded
    let encoder = JSONEncoder()
    if let encoded = try? encoder.encode(superClassArray){
        UserDefaults.standard.set(encoded, forKey: " superClassArray ")
    } else {
        print("Save didn't work!")
    }
}

Then, instead of creating an empty superClassArray, then using: superClassArray = subClassArray, as shown above, I replace this with the single line:

let superClassArray: [SuperClass] = subClassArray.map{SuperClass(name:  $0.name, coordinates: $0.coordinates)}

This works. Again, assumption is because I'm passing in the values inside of the class reference type & haven't made the superClassArray = subClassArray. Also, as expected, subClassArray[0] === superClassArray[0] is false

So why did the "old stuff" in Swift 3 work, even though I used the line superClassArray = subClassArray before the let superClassData = NSKeyedArchiver.archivedData(withRootObject: superClassArray) ? Am I essentially achieving the same result by creating the array in Swift 4 that was happening with the old Swift 3 encoder/decoder? Is the looping / recreation

Thanks!

1
Catch the error thrown by the JSONEncoder and print it out. It'll give a better idea of why encoding is failing. Also if you can include the definition of your classes (or some simplified version that demonstrates the issue) it is more likely someone will be able to help you!idz

1 Answers

4
votes

Polymorphic persistence appears to be broken by design.

The bug report SR-5331 quotes the response they got on their Radar.

Unlike the existing NSCoding API (NSKeyedArchiver), the new Swift 4 Codable implementations do not write out type information about encoded types into generated archives, for both flexibility and security. As such, at decode time, the API can only use the concrete type your provide in order to decode the values (in your case, the superclass type).

This is by design — if you need the dynamism required to do this, we recommend that you adopt NSSecureCoding and use NSKeyedArchiver/NSKeyedUnarchiver

I am unimpressed, having thought from all the glowing articles that Codable was the answer to some of my prayers. A parallel set of Codable structs that act as object factories is one workaround I'm considering, to preserve type information.

Update I have written a sample using a single struct that manages recreating polymorphic classes. Available on GitHub.

I was not able to get it to work easily with subclassing. However, classes that conform to a base protocol can apply Codable for default encoding. The repo contains both keyed and unkeyed approaches. The simpler is unkeyed, copied below

// Demo of a polymorphic hierarchy of different classes implementing a protocol
// and still being Codable
// This variant uses unkeyed containers so less data is pushed into the encoded form.
import Foundation

protocol BaseBeast  {
  func move() -> String
  func type() -> Int
  var name: String { get }
}

class DumbBeast : BaseBeast, Codable  {
  static let polyType = 0
  func type() -> Int { return DumbBeast.polyType }

  var name:String
  init(name:String) { self.name = name }
  func move() -> String { return "\(name) Sits there looking stupid" }
}

class Flyer : BaseBeast, Codable {
  static let polyType = 1
  func type() -> Int { return Flyer.polyType }

  var name:String
  let maxAltitude:Int
  init(name:String, maxAltitude:Int) {
    self.maxAltitude = maxAltitude
    self.name = name
  }
  func move() -> String { return "\(name) Flies up to \(maxAltitude)"}
}


class Walker : BaseBeast, Codable {
  static let polyType = 2
  func type() -> Int { return Walker.polyType }

  var name:String
  let numLegs: Int
  let hasTail: Bool
  init(name:String, legs:Int=4, hasTail:Bool=true) {
    self.numLegs = legs
    self.hasTail = hasTail
    self.name = name
  }
  func move() -> String {
    if numLegs == 0 {
      return "\(name) Wriggles on its belly"
    }
    let maybeWaggle = hasTail ? "wagging its tail" : ""
    return "\(name) Runs on \(numLegs) legs \(maybeWaggle)"
  }
}

// Uses an explicit index we decode first, to select factory function used to decode polymorphic type
// This is in contrast to the current "traditional" method where decoding is attempted and fails for each type
// This pattern of "leading type code" can be used in more general encoding situations, not just with Codable
//: **WARNING** there is one vulnerable practice here - we rely on the BaseBeast types having a typeCode which
//: is a valid index into the arrays `encoders` and `factories`
struct CodableRef : Codable {
  let refTo:BaseBeast  //In C++ would use an operator to transparently cast CodableRef to BaseBeast

  typealias EncContainer = UnkeyedEncodingContainer
  typealias DecContainer = UnkeyedDecodingContainer
  typealias BeastEnc = (inout EncContainer, BaseBeast) throws -> ()
  typealias BeastDec = (inout DecContainer) throws -> BaseBeast

  static var encoders:[BeastEnc] = [
    {(e, b) in try e.encode(b as! DumbBeast)},
    {(e, b) in try e.encode(b as! Flyer)},
    {(e, b) in try e.encode(b as! Walker)}
  ]

  static var factories:[BeastDec] = [
    {(d) in try d.decode(DumbBeast.self)},
    {(d) in try d.decode(Flyer.self)},
    {(d) in try d.decode(Walker.self)}
  ]

  init(refTo:BaseBeast) {
    self.refTo = refTo
  }

  init(from decoder: Decoder) throws {
    var container = try decoder.unkeyedContainer()
    let typeCode = try container.decode(Int.self)
    self.refTo = try CodableRef.factories[typeCode](&container)
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.unkeyedContainer()
    let typeCode = self.refTo.type()
    try container.encode(typeCode)
    try CodableRef.encoders[typeCode](&container, refTo)
  }
}


struct Zoo : Codable {
  var creatures = [CodableRef]()
  init(creatures:[BaseBeast]) {
    self.creatures = creatures.map {CodableRef(refTo:$0)}
  }
  func dump() {
    creatures.forEach { print($0.refTo.move()) }
  }
}


//: ---- Demo of encoding and decoding working ----
let startZoo = Zoo(creatures: [
  DumbBeast(name:"Rock"),
  Flyer(name:"Kookaburra", maxAltitude:5000),
  Walker(name:"Snake", legs:0),
  Walker(name:"Doggie", legs:4),
  Walker(name:"Geek", legs:2, hasTail:false)
  ])


startZoo.dump()
print("---------\ntesting JSON\n")
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let encData = try encoder.encode(startZoo)
print(String(data:encData, encoding:.utf8)!)
let decodedZoo = try JSONDecoder().decode(Zoo.self, from: encData)

print ("\n------------\nAfter decoding")

decodedZoo.dump()

Update 2020-04 experience

This approach continues to be more flexible and superior to using Codable, at the cost of a bit more programmer time. It is used very heavily in the Touchgram app which provides rich, interactive documents inside iMessage.

There, I need to encode multiple polymorphic hierarchies, including different Sensors and Actions. By storing signatures of decoders, it not only provides with subclassing but also allows me to keep older decoders in the code base so old messages are still compatible.