2

NSCoding requires init(coder:), but there is also the optional version of this method init?(coder:).

What exactly should one do if this returns nil? Is this even an issue?

Say you are initializing a large hierarchy of objects with init(coder:), with each object's child objects themselves being initialized using init?(coder:). If somewhere along the way one of those objects is nil, wouldn't the app crash? The parent object is not expecting a nil child.

What does it even mean to "init nil"?

  class Parent: NSCoding {

       var children: [Child]

       required init?(coder aDecoder: NSCoder) {
          guard let children = aDecoder.decodeObject(forKey: "children") as? [Child] else { return nil }
          self.children = children
       }

  }

  class Child: NSCoding {
        var name: String

        required init?(coder aDecoder: NSCoder) {
            guard let name = aDecoder.decodeObject(forKey: "name") as? String else { return nil }
            self.name = name
        }

  }

One strategy would be to simply return a new instance rather than simply returning nil. The data would be lost, but it the app would run.

MH175
  • 2,234
  • 1
  • 19
  • 35
  • 2
    If you are *encoding* only non-optional properties the *decoding* `init` method cannot return `nil`. The `guard` is not needed either in this case. – vadian May 11 '17 at 04:17
  • 1
    If you are very sure that it shouldn't be nil then `self.name = aDecoder.decodeObject(forKey: "name") as! String`. If you dont want to risk crashing, one way is to initialize with a default value `self.name = aDecoder.decodeObject(forKey: "name") as? String ?? ""`. – xiangxin May 11 '17 at 08:08

1 Answers1

1

You'd better not return nil.

As my test in Xcode 8.3.2 (8E2002), return nil in init(coder:) cause NSKeyedUnarchiver.unarchiveObject crash or return unexpected result.

Prepare a class which encode wrong data type for "test2":

class MyClass: NSObject, NSCoding {
    var x: String

    init(_ x: String) {
        self.x = x
    }

    required init?(coder aDecoder: NSCoder) {
        guard let x = aDecoder.decodeObject(forKey: "x") as? String else {
            return nil
        }
        self.x = x
    }

    func encode(with aCoder: NSCoder) {
        if x == "test2" {
            aCoder.encode(Int(4), forKey: "x")
        } else {
            aCoder.encode(x, forKey: "x")
        }
    }
}

TestCaseA: archive a dictionary which contains above MyClass, then unarchive.

Result: crash on NSKeyedUnarchiver.unarchiveObject.

    let encodedData = NSKeyedArchiver.archivedData(withRootObject: [
        "k1":MyClass("test1"),
        "k2":MyClass("test2"),
        "k3":"normal things"
        ])
    UserDefaults.standard.set(encodedData, forKey: "xx")


    if let data = UserDefaults.standard.data(forKey: "xx"),
        let _data = NSKeyedUnarchiver.unarchiveObject(with: data) {
        if let dict = _data as? [String:Any] {
            debugPrint(dict.count)
        }
    }

TestCaseB: archive an array which contains above MyClass, then unarchive.

Result: return an empty array (but expected is an array with 1 element)

    let encodedData = NSKeyedArchiver.archivedData(withRootObject: [
        MyClass("test1"),
        MyClass("test2")
        ])
    UserDefaults.standard.set(encodedData, forKey: "xx")


    if let data = UserDefaults.standard.data(forKey: "xx"),
        let _data = NSKeyedUnarchiver.unarchiveObject(with: data) {
        if let dict = _data as? [Any] {
            debugPrint(dict.count)
        }
    }
osexp2000
  • 2,910
  • 30
  • 29
  • I think this article makes an excellent point http://www.jessesquires.com/swift-failable-initializers-revisited/. We should not be returning nil in an init and instead have some sort of pre-init validation. – MH175 May 13 '17 at 14:26