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.

2
  • 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. Commented May 11, 2017 at 4: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 ?? "". Commented May 11, 2017 at 8:08

1 Answer 1

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)
        }
    }
Sign up to request clarification or add additional context in comments.

1 Comment

I think this article makes an excellent point jessesquires.com/swift-failable-initializers-revisited. We should not be returning nil in an init and instead have some sort of pre-init validation.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.