I'm making a to-do list app to learn the ins and outs and it has a simple lists-with-items structure. I use 3 classes to manage them:
- TodoManager: is a singleton that is meant to centralise managing lists and items in those lists in my view controllers. It holds an array of TodoLists and a bunch of functions to add lists, mark them as completed and return lists.
- TodoList: has a string var (name), bool var (completed) and an array of TodoItems
- TodoItem: has a string var (name) and a bool var (completed).
I want to store my array of custom objects [TodoList] so I can load it later and I was looking for the simplest way in the world to do it. UserDefaults does not allow custom objects (as it shouldn't because it's for settings) so I need to persist the data using NSCoding and for that I need to have my TodoList class inherit from NSObjects.
class TodoManager: NSObject, NSCoding {
// the singleton
static let shared = TodoManager()
// filePath var
private var filePath : String {
let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
return url.appendingPathComponent("objectsArray").path
}
// init
override private init() {}
// array to store all lists
private var lists = [TodoList]()
func newTodoList(title: String) {
lists.append(TodoList(instanceTitle: title))
}
// coding
func encode(with coder: NSCoder) {
coder.encode(lists, forKey: "lists")
}
required convenience init(coder decoder: NSCoder) {
self.init()
lists = decoder.decodeObject(forKey: "lists") as! [TodoList]
}
// saving and loading
func saveAll() {
let data = lists
NSKeyedArchiver.archiveRootObject(data, toFile: filePath)
}
func loadAll() {
if let dataArray = NSKeyedUnarchiver.unarchiveObject(withFile: filePath) as? [TodoList] {
lists = dataArray
}
}
}
class TodoList: NSObject, NSCoding {
// array to store items in this list instance
private var items = [TodoItem]()
// vars to store title and completion status
private var title = String()
private var completed = Bool()
// init
init(instanceTitle: String) {
title = instanceTitle
completed = false
}
// coding
func encode(with coder: NSCoder) {
coder.encode(items, forKey: "lists")
coder.encode(title, forKey: "title")
coder.encode(completed, forKey: "completed")
}
required convenience init(coder decoder: NSCoder) {
self.items = decoder.decodeObject(forKey: "items") as! [TodoItem]
self.title = decoder.decodeObject(forKey: "title") as! String
self.completed = decoder.decodeBool(forKey: "completed")
self.init() // <----- critical line
}
// item-related
func addItem(title: String) {
items.append(TodoItem(instanceTitle: title))
}
}
class TodoItem: NSObject, NSCoding {
private var title = String()
private var completed = Bool()
// inits
init(instanceTitle: String) {
title = instanceTitle
completed = false
}
func encode(with coder: NSCoder) {
coder.encode(title, forKey: "title")
coder.encode(completed, forKey: "completed")
}
required convenience init(coder decoder: NSCoder) {
self.init() // <----- similar critical line
title = decoder.decodeObject(forKey: "title") as! String
completed = decoder.decodeBool(forKey: "completed")
}
}
The problem I run into is that instanceTitle is undeclared when in the convenience init so I can't pass it to self.init(). I cannot add the declaration to the required convenience init because it will error that the class does not conform to the required protocol. I tried a good many variations but after hours of staring at this and using my google-foo I can't figure it out. What am I doing wrong in NSCoding's rabbit hole?