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:
//You can't encode optional values, so you don't use `?` here.
var dueDate: Date
var assignmentImage: UIImage
Initializer:
//Removed the `?` after UIImage because it isn't an Optional.
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 //This String should use `!` instead of `?`.
let materials = aDecoder.decodeObject(forKey: PropertyKey.materialsKey) as! String //This String should use `!` instead of `?`.
let dueDate = aDecoder.decodeObject(forKey: PropertyKey.dueDateKey) as! Date
//Check for the UIImage being nil. If it is, assign some default image to it so that it doesn't unwrap nil and crash.
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.
//`materials` and `assignmentDescription` don't need `!` now because they're already unwrapped.
self.init(name: name, className: className, assignmentDescription: assignmentDescription, materials: materials, dueDate: dueDate, assignmentImage: assignmentImage)
It isn't perfect, but it works.
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)
}
I just created a sample project called Swift3PhoneTest on Github (link) that demonstrates using NSSecureCoding
to save a custom data container object.
It has both a non-optional and an optional property, and properly manages archiving and unarchiving when the optional property is nil.