Search code examples
iosunit-testingcllocationnskeyedarchivernskeyedunarchiver

Apparently, CLLocation objects cannot be archived / unarchived precisely


In my app (only relevant code shown), I have a class Test with a property

var location: CLLocation  

I archive it using

public func encode(with aCoder: NSCoder) {
    aCoder.encode(location, forKey: "location")
}  

And it is unarchived using

required convenience public init?(coder aDecoder: NSCoder) {
    let unarchivedLocation = aDecoder.decodeObject(forKey: "location") as! CLLocation
    self.init(location: unarchivedLocation)
}  

Unit testing is done using

func test_archiningUnarchiving() {
    // given
    let location = CLLocation.init(latitude: 0.0, longitude: 0.0)
    let test = Test(location: location)
    // when
    let data = NSKeyedArchiver.archivedData(withRootObject: test)
    let unarchivedTest = NSKeyedUnarchiver.unarchiveObject(with: data) as? Test
    // then
    XCTAssertEqual(unarchivedTest!.location, location, "location was not correctly unarchived")
}  

This test fails:

XCTAssertEqual failed: ("<+0.00000000,+0.00000000> +/- 0.00m (speed -1.00 mps / course -1.00) @ 1/19/18, 3:30:50 PM Central European Standard Time") is not equal to ("<+0.00000000,+0.00000000> +/- 0.00m (speed -1.00 mps / course -1.00) @ 1/19/18, 3:30:50 PM Central European Standard Time") - location was not correctly unarchived

The log shows twice exactly the same data for the original and the unarchived location.
Any idea what could have gone wrong anyway??


Solution

  • The problem is not archiving, but rather the equality test. If you compare two different CLLocation instances, even if they're identical, it will always return false.

    Bottom line, any NSObject subclass that doesn't explicitly implement isEqual: (such as is the case with CLLocation) will experience this behavior.

    The Using Swift with Cocoa and Objective-C: Interacting with Objective-C APIs says:

    Swift provides default implementations of the == and === operators and adopts the Equatable protocol for objects that derive from the NSObject class. The default implementation of the == operator invokes the isEqual: method ... You should not override the equality or identity operators for types imported from Objective-C.

    And, the Concepts in Objective-C Programming: Introspection tells us:

    The default NSObject implementation of isEqual: simply checks for pointer equality.

    Personally, I wish that NSObject subclasses didn't automatically inherit isEqual:, but it is what it is.

    Bottom line, don't attempt to test for equality of NSObject subclasses unless you know it has properly implemented isEqual: override. If you want, write your own method, e.g. isEqual(to location: CLLocation) (but not isEqual(_:)) that performs memberwise comparison of two CLLocation objects.