3

How should errors related to NSCoding be handled in Swift?

When an object is initialized using init?(coder:) it may fail to be initialized if the data is invalid. I'd like to catch these errors and appropriately handle them. Why is init?(coder:) not defined as a throwing function in Swift?

Anthony Mattox
  • 7,048
  • 6
  • 43
  • 59

1 Answers1

5

NSCoding defines it as Optional:

init?(coder aDecoder: NSCoder)

So it is certainly possible to detect errors.

90% of "why does Swift...?" questions can be answered with "because Cocoa." Cocoa does not define initWithCoder: as returning an error, so it does not translate to throws in Swift. There would be no way to cleanly bridge it to existing code. (NSCoding goes back NeXTSTEP. We've built a lot of software without returning an NSError there. Doesn't mean it might not be nice sometimes, but "couldn't init" has traditionally been enough.)

Check for nil. That means that something failed. That is all the information that is provided.


I've never in practice had to check too deeply that the entire object graph was correct. If it isn't, you're incredibly likely to get other errors anyway, and remember that NSKeyedUnarchiver will raise an ObjC exception (!!!) if it fails to decode. Unless you wrap this in an ObjC @catch, you're going to crash anyway. (And yes, that's pretty crazy, but still true.)

But if I wanted to be extremely careful and make sure that things I expected to be in the archive were really in the archive (even if they were nil), I might do it this way (untested; it compiles but I haven't made sure it really works):

import Foundation

enum DecodeError: ErrorType {
    case MissingProperty(String)
    case MalformedProperty(String)
}

extension NSCoder {
    func encodeOptionalObject(obj: AnyObject?, forKey key: String) {
        let data = obj.map{ NSKeyedArchiver.archivedDataWithRootObject($0) } ?? NSData()
        self.encodeObject(data, forKey: key)
    }

    func decodeRequiredOptionalObjectForKey(key: String) throws -> AnyObject? {
        guard let any = self.decodeObjectForKey(key) else {
            throw DecodeError.MissingProperty(key)
        }

        guard let data = any as? NSData else {
            throw DecodeError.MalformedProperty(key)
        }

        if data.length == 0 {
            return nil // Found nil
        }

        // But remember, this will raise an ObjC exception if it's malformed data!
        guard let prop = NSKeyedUnarchiver.unarchiveObjectWithData(data) else {
            throw DecodeError.MalformedProperty(key)
        }

        return prop
    }
}

class MyClass: NSObject, NSCoding {
    static let propertyKey = "property"
    let property: String?
    init(property: String?) {
        self.property = property
    }
    required init?(coder aDecoder: NSCoder) {
        do {
            property = try aDecoder.decodeRequiredOptionalObjectForKey(MyClass.propertyKey) as? String
        } catch {
            // do something with error if you want
            property = nil
            super.init()
            return nil
        }
        super.init()
    }

    func encodeWithCoder(aCoder: NSCoder) {
        aCoder.encodeOptionalObject(property, forKey: MyClass.propertyKey)
    }
}

As I said, I've never actually done this in a Cocoa program. If anything were really corrupted in the archive, you're almost certainly going to wind up raising an ObjC exception, so all this error checking is likely overkill. But it does let you distinguish between "nil" and "not in the archive." I just encode the property individually as an NSData and then encode the NSData. If it's nil, I encode an empty NSData.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • That makes sense. You're point that it's building off existing Cocoa behavior is entirely valid. I think this might be a problem in my understanding of `NSCoding`. Doing a similar thing in Objective-C I have wrapped loading the top level object of a graph of some complexity in a try/catch block, but the errors here must actually be me assigning nil values to non-optional properties when something can not be decoded during initialization. Instead the entire initializer should return nil if any of the values are nil. Does that sound correct? – Anthony Mattox Oct 16 '15 at 18:48
  • Correct. You need to return nil (which also means, unfortunately today) that you have to find *some* way to initialize all properties. You *must* init everything before returning nil, which the Swift team acknowledges is a bit broken. – Rob Napier Oct 16 '15 at 19:00
  • Got it. I haven't digested failable initializers yet, but I can learn that. Do you have suggestions on handling valid nil values? For example: in an archive object of type A has an optional property of type B. If instance of type B can not be initialized it's `init?(coder:)` returns nil. Object A will have it's property initialized with `nil` and be in a valid state. I'd prefer to have object A still fail because if any properties down the graph fail, it's not meaningful. Does that make sense? Is there a way to handle that in this case? – Anthony Mattox Oct 16 '15 at 19:15
  • Updated. This is almost certainly overkill, and if you don't wrap this code in an ObjC `@catch`, you'll probably crash anyway. But it's how I might attack it. – Rob Napier Oct 16 '15 at 20:30
  • Thanks for exploring this so thoroughly! I think the other point I didn't quite get initially is the difference between returning and error and raising an exception and how that translates (or doesn't) do Swift. I'm not happy to include the Objective-C catch, but that's probably the simplest thing to at least get a working iteration. – Anthony Mattox Oct 17 '15 at 13:37