0
votes

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?

1

1 Answers

0
votes

When the convenience initializer() is called, the actual arguments originally passed to the designated init() by the function that is requesting a new instance of the class to be made do not need to pass through the convenience init. You call the designated initializer with a placeholder which is filled in on execution. For my case it looks like this:

init(instanceTitle: String) {
    title = instanceTitle
    completed = false
}

required convenience init(coder decoder: NSCoder) {
    // call designated init
    self.init(instanceTitle: "[placeholder]")
    items = decoder.decodeObject(forKey: "items") as! [TodoItem]
    title = decoder.decodeObject(forKey: "title") as! String
    completed = decoder.decodeBool(forKey: "completed")
}