H. Martin H. Martin - 14 days ago 16
Swift Question

Encode/Decoding Date with NSCoder in Swift 3?

I'm writing a planner app, based on Apple's FoodTracker tutorial, that stores several strings, a Date, and a UIImage per Assignment. I'm encountering problems encoding/decoding the Date. I don't know for sure which it is because the console outputs change with every slight modification, but from what I can tell, my code saves the Date as nil, and then when it tries to load that Date, it unexpectedly finds nil and crashes. Because I'm relatively new to Swift and Swift 3 is a headache and a half, I have very little idea where the problem really is. Here's the code that I think should work:

class Assignment: NSObject, NSCoding {

//MARK: Properties

var name: String
var className: String
var assignmentDescription: String
var materials: String
var dueDate: Date?
var assignmentImage: UIImage?

//MARK: Archiving Paths

static let DocumentsDirectory = FileManager().urls(for: .documentDirectory, in: .userDomainMask).first!
static let ArchiveURL = DocumentsDirectory.appendingPathComponent("assignments")

//MARK: Types

struct PropertyKey {
static let nameKey = "name"
static let classNameKey = "className"
static let assignmentDescriptionKey = "assignmentDescription"
static let materialsKey = "materials"
static let dueDateKey = "dueDate"
static let assignmentImageKey = "assignmentImage"
}

//MARK: Initialization

init?(name: String, className: String, assignmentDescription: String, materials: String, dueDate: Date, assignmentImage: UIImage?) {
//Initialize stored properties.
self.name = name
self.className = className
self.assignmentDescription = assignmentDescription
self.materials = materials
self.dueDate = dueDate
self.assignmentImage = assignmentImage

super.init()

//Initialization should fail if there is no name and no class.
if name.isEmpty && className.isEmpty {
print("Failed to initialize an assignment.")
return nil
}
}

//MARK: NSCoding

func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: PropertyKey.nameKey)
aCoder.encode(className, forKey: PropertyKey.classNameKey)
aCoder.encode(assignmentDescription, forKey: PropertyKey.assignmentDescriptionKey)
aCoder.encode(materials, forKey: PropertyKey.materialsKey)
aCoder.encode(dueDate, forKey: PropertyKey.dueDateKey)
aCoder.encode(assignmentImage, forKey: PropertyKey.dueDateKey)
}

required convenience init?(coder aDecoder: NSCoder) {
//Required fields.
let name = aDecoder.decodeObject(forKey: PropertyKey.nameKey) as! String
let className = aDecoder.decodeObject(forKey: PropertyKey.classNameKey) as! String

//Optional fields.
let assignmentDescription = aDecoder.decodeObject(forKey: PropertyKey.assignmentDescriptionKey) as? String
let materials = aDecoder.decodeObject(forKey: PropertyKey.materialsKey) as? String
let dueDate = aDecoder.decodeObject(forKey: PropertyKey.dueDateKey) as! Date
let assignmentImage = aDecoder.decodeObject(forKey: PropertyKey.assignmentImageKey) as? UIImage

//Must call designated initializer.
self.init(name: name, className: className, assignmentDescription: assignmentDescription!, materials: materials!, dueDate: dueDate, assignmentImage: assignmentImage)
}


Any insight at all would be appreciated.

Edit:

With Duncan C's help and Xcode's fix-its, this is what
required convenience init?(coder aDecoder: NSCoder)
looks like now:

//Required fields.
let newName = aDecoder.decodeObject(forKey: PropertyKey.nameKey) as! String
let newClassName = aDecoder.decodeObject(forKey: PropertyKey.classNameKey) as! String

//Optional fields.
var newAssignmentImage: UIImage?
var newDueDate: Date

let newAssignmentDescription = aDecoder.decodeObject(forKey: PropertyKey.assignmentDescriptionKey) as! String
let newMaterials = aDecoder.decodeObject(forKey: PropertyKey.materialsKey) as! String

if aDecoder.containsValue(forKey: PropertyKey.dueDateKey) {
newDueDate = aDecoder.decodeObject(forKey: PropertyKey.dueDateKey) as! Date
} else {
newDueDate = Date()
if aDecoder.containsValue(forKey: PropertyKey.assignmentImageKey) {
newAssignmentImage = aDecoder.decodeObject(forKey: PropertyKey.assignmentImageKey) as? UIImage
} else {
newAssignmentImage = UIImage(named: "sampleAssignmentImage")
}
}

//Must call designated initializer.
self.init(name: newName, className: newClassName, assignmentDescription: newAssignmentDescription, materials: newMaterials, dueDate: newDueDate, assignmentImage: newAssignmentImage)!


It compiles, but it still throws
fatal error: unexpectedly found nil while unwrapping an Optional value
on the line
newDueDate = aDecoder.decodeObject(forKey: PropertyKey.dueDateKey) as! Date


Edit 2:

After going back to my original code and changing
?
to
!
, this is what my code looks like:

//Required fields.
let name = aDecoder.decodeObject(forKey: PropertyKey.nameKey) as! String
let className = aDecoder.decodeObject(forKey: PropertyKey.classNameKey) as! String

//Optional fields.
let assignmentDescription = aDecoder.decodeObject(forKey: PropertyKey.assignmentDescriptionKey) as! String
let materials = aDecoder.decodeObject(forKey: PropertyKey.materialsKey) as! String
let dueDate = aDecoder.decodeObject(forKey: PropertyKey.dueDateKey) as! Date
let assignmentImage = aDecoder.decodeObject(forKey: PropertyKey.assignmentImageKey) as! UIImage

//Must call designated initializer.
self.init(name: name, className: className, assignmentDescription: assignmentDescription, materials: materials, dueDate: dueDate, assignmentImage: assignmentImage)


It compiles, but it still throws
fatal error: unexpectedly found nil while unwrapping an Optional value
on the line
let dueDate = aDecoder.decodeObject(forKey: PropertyKey.dueDateKey) as! Date


Edit 3 (Working):

For anyone interested, here are the sections of the original code that were modified to make it work:

Properties:

var dueDate: Date
var assignmentImage: UIImage


Initializer:

init?(name: String, className: String, assignmentDescription: String, materials: String, dueDate: Date, assignmentImage: UIImage)


Decoder (
required convenience init?(coder aDecoder: NSCoder)
):

//Required fields.
let name = aDecoder.decodeObject(forKey: PropertyKey.nameKey) as! String
let className = aDecoder.decodeObject(forKey: PropertyKey.classNameKey) as! String

//Optional fields.
let assignmentDescription = aDecoder.decodeObject(forKey: PropertyKey.assignmentDescriptionKey) as! String
let materials = aDecoder.decodeObject(forKey: PropertyKey.materialsKey) as! String
let dueDate = aDecoder.decodeObject(forKey: PropertyKey.dueDateKey) as! Date
var assignmentImage: UIImage!
if aDecoder.decodeObject(forKey: PropertyKey.assignmentImageKey) == nil {
assignmentImage = UIImage(named: "SampleAssignmentImage")
}
else {
assignmentImage = aDecoder.decodeObject(forKey: PropertyKey.assignmentImageKey) as! UIImage
}

//Must call designated initializer.
self.init(name: name, className: className, assignmentDescription: assignmentDescription, materials: materials, dueDate: dueDate, assignmentImage: assignmentImage)


It isn't perfect, but it works.

Answer

As Matt, says, you can't encode an optional. Rather than force-unwrapping it, though, I would suggest adding an if let and only adding the optionals to the archive if they contain a value:

func encode(with aCoder: NSCoder) {
        aCoder.encode(name, forKey: PropertyKey.nameKey)
        aCoder.encode(className, forKey: PropertyKey.classNameKey)
        aCoder.encode(assignmentDescription, forKey: PropertyKey.assignmentDescriptionKey)
        aCoder.encode(materials, forKey: PropertyKey.materialsKey)
        if let date = dueDate {
            aCoder.encode(date, forKey: PropertyKey.dueDateKey)

        }
        if let image = assignmentImage {
            aCoder.encode(image, forKey: PropertyKey.dueDateKey)
        }
    }

And then in your init(coder:) method, check to see if the keys exist before decoding:

required convenience init?(coder aDecoder: NSCoder) {
    //Required fields.
    name = aDecoder.decodeObject(forKey: PropertyKey.nameKey) as! String
    className = aDecoder.decodeObject(forKey: PropertyKey.classNameKey) as! String

    //Optional fields.
    assignmentDescription = aDecoder.containsValue(forKey: PropertyKey.assignmentDescriptionKey) as? String
    materials = aDecoder.decodeObject(forKey: PropertyKey.materialsKey) as? String

    if aDecoder.containsValue(forKey: PropertyKey.dueDateKey) {
        dueDate = aDecoder.decodeObject(forKey: PropertyKey.dueDateKey) as! Date
    } else {
       dueDate = nil
    if aDecoder.containsValue(forKey: PropertyKey.assignmentImageKey) {
        assignmentImage = aDecoder.decodeObject(forKey: PropertyKey.assignmentImageKey) as? UIImage
    } else {
      assignmentImage = nil
    }

    //Must call designated initializer.
    self.init(name: name, className: className, assignmentDescription: assignmentDescription!, materials: materials!, dueDate: dueDate, assignmentImage: assignmentImage)
}
Comments