How to Test Required Init(Coder:)

How to test required init(coder:)?

Production code:

required init?(coder: NSCoder) {
return nil
}

Test:

func testInitWithCoder() {
let archiverData = NSMutableData()
let archiver = NSKeyedArchiver(forWritingWithMutableData: archiverData)
let someView = SomeView(coder: archiver)
XCTAssertNil(someView)
}

Since the required initializer returns nil and does not use the coder, the above code can be simplified to:

func testInitWithCoder() {
let someView = SomeView(coder: NSCoder())
XCTAssertNil(someView)
}

The optionality of init?(coder:) vs init(coder:). What to do if nil?

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)
}
}

UIViewController and required init?(coder: )

Remove

fatalError("init(coder:) has not been implemented")

line from required init?(coder aDecoder: NSCoder) initialiser. Your error is thrown by this line. This line is not needed.

XCode adds this line when you create init?(coder aDecoder: NSCoder) method to remind developer to implement this method or to never call it. If developer will forget to implement it but will try to instantiate a class from archive or Storyboard he will receive an assert and will be able to find a bug easily.

Swift : Error: 'required' initializer 'init(coder:)' must be provided by subclass of 'UIView'

Note for required: Write the required modifier before the definition of a class initializer to indicate that every subclass of the class must implement that initializer.

Note for override: You always write the override modifier when overriding a superclass designated initializer, even if your subclass’s implementation of the initializer is a convenience initializer.

Above both notes are referred from: Swift Programming Language/Initialization

Therefore, your subclass of UIView should look similar to the sample below:

class MyView: UIView {
...
override init(frame: CGRect) {
super.init(frame: frame)
}

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
...
}

Is it OK to have fatalError in required init?(coder aDecoder: NSCoder) when I don't use Storyboards?

Since you don't use storyboard you can disable your init, so you won't be able to use it in code:

@available(*, unavailable) required init?(coder aDecoder: NSCoder) {
fatalError("disabled init")
}

Swift 2.0 Unit Testing for NSCoding

Walking away from a problem always helps.

func testDecoder() {

let path = NSTemporaryDirectory() as NSString
let locToSave = path.stringByAppendingPathComponent("teststasks")

let newTask = Task(name: "savename", time: "10")

// save tasks
NSKeyedArchiver.archiveRootObject([newTask], toFile: locToSave)

// load tasks
let data = NSKeyedUnarchiver.unarchiveObjectWithFile(locToSave) as? [Task]

XCTAssertNotNil(data)
XCTAssertEqual(data!.count, 1)
XCTAssertEqual(data!.first?.name, "savename")
XCTAssertEqual(data!.first?.time, 10)
}


Related Topics



Leave a reply



Submit