In the previous lesson of Swift From Scratch, we created a functional to-do application. The data model could use some love, though. In this final lesson, we’re going to refactor the data model by implementing a custom model class.
1. The Data Model
The data model we’re about to implement includes two classes, a Task class and a ToDo class that inherits from the Task class. While we create and implement these model classes, we’ll continue our exploration of object-oriented programming in Swift. In this lesson, we’ll zoom in on the initialization of class instances and what role inheritance plays during initialization.
The Task Class
Let’s start with the implementation of the Task class. Create a new Swift file by selecting New > File… from Xcode’s File menu. Choose Swift File from the iOS > Source section. Name the file Task.swift and hit Create.

The basic implementation is short and simple. The Task class inherits from NSObject, defined in the Foundation framework, and has a variable property name of type String. The class defines two initializers, init() and init(name:). There are a few details that might trip you up, so let me explain what’s happening.
import Foundation
class Task: NSObject {
var name: String
convenience override init() {
self.init(name: "New Task")
}
init(name: String) {
self.name = name
}
}
Because the init() method is also defined in the NSObject class, we need to prefix the initializer with the override keyword. We covered overriding methods earlier in this series. In the init() method, we invoke the init(name:) method, passing in "New Task" as the value for the name parameter.
The init(name:) method is another initializer, accepting a single parameter name of type String. In this initializer, the value of the name parameter is assigned to the name property. This is easy enough to understand. Right?
Designated and Convenience Initializers
What’s with the convenience keyword prefixing the init() method? Classes can have two types of initializers, designated initializers and convenience initializers. Convenience initializers are prefixed with the convenience keyword, which implies that init(name:) is a designated initializer. Why is that? What’s the difference between designated and convenience initializers?
Designated initializers fully initialize an instance of a class, meaning that every property of the instance has an initial value after initialization. Looking at the Task class, for example, we see that the name property is set with the value of the name parameter of the init(name:) initializer. The result after initialization is a fully initialized Task instance.
Convenience initializers, however, rely on a designated initializer to create a fully initialized instance of the class. That’s why the init() initializer of the Task class invokes the init(name:) initializer in its implementation. This is referred to as initializer delegation. The init() initializer delegates initialization to a designated initializer to create a fully initialized instance of the Task class.
Convenience initializers are optional. Not every class has a convenience initializer. Designated initializers are required, and a class needs to have at least one designated initializer to create a fully initialized instance of itself.
The NSCoding Protocol
The implementation of the Task class isn’t complete, though. Later in this lesson, we will write an array of ToDo instances to disk. This is only possible if instances of the ToDo class can be encoded and decoded.
Don’t worry, though—this isn’t rocket science. We only need to make the Task and ToDo classes conform to the NSCoding protocol. That’s why the Task class inherits from the NSObject class since the NSCoding protocol can only be implemented by classes inheriting—directly or indirectly—from NSObject. Like the NSObject class, the NSCoding protocol is defined in the Foundation framework.
Adopting a protocol is something we already covered in this series, but there are a few gotchas that I want to point out. Let’s start by telling the compiler that the Task class conforms to the NSCoding protocol.
import Foundation
class Task: NSObject, NSCoding {
var name: String
...
}
Next, we need to implement the two methods declared in the NSCoding protocol, init?(coder:) and encode(with:). The implementation is straightforward if you’re familiar with the NSCoding protocol.
import Foundation
class Task: NSObject, NSCoding {
var name: String
@objc required init?(coder aDecoder: NSCoder) {
name = aDecoder.decodeObject(forKey: "name") as! String
}
@objc func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: "name")
}
convenience override init() {
self.init(name: "New Task")
}
init(name: String) {
self.name = name
}
}
The init?(coder:) initializer is a designated initializer that initializes a Task instance. Even though we implement the init?(coder:) method to conform to the NSCoding protocol, you won’t ever need to invoke this method directly. The same is true for encode(with:), which encodes an instance of the Task class.
The required keyword prefixing the init?(coder:) method indicates that every subclass of the Task class needs to implement this method. The required keyword only applies to initializers, which is why we don’t need to add it to the encode(with:) method.
Before we move on, we need to talk about the @objc attribute. Because the NSCoding protocol is an Objective-C protocol, protocol conformance can only be checked by adding the @objc attribute. In Swift, there’s no such thing as protocol conformance or optional protocol methods. In other words, if a class adheres to a particular protocol, the compiler verifies and expects that every method of the protocol is implemented.
The ToDo Class
With the Task class implemented, it’s time to implement the ToDo class. Create a new Swift file and name it ToDo.swift. Let’s look at the implementation of the ToDo class.
import Foundation
class ToDo: Task {
var done: Bool
@objc required init?(coder aDecoder: NSCoder) {
self.done = aDecoder.decodeBool(forKey: "done")
super.init(coder: aDecoder)
}
@objc override func encode(with aCoder: NSCoder) {
aCoder.encode(done, forKey: "done")
super.encode(with: aCoder)
}
init(name: String, done: Bool) {
self.done = done
super.init(name: name)
}
}
The ToDo class inherits from the Task class and declares a variable property done of type Bool. In addition to the two required methods of the NSCoding protocol that it inherits from the Task class, it also declares a designated initializer, init(name:done:).
As in Objective-C, the super keyword refers to the superclass, the Task class in this example. There is one important detail that deserves attention. Before you invoke the init(name:) method on the superclass, every property declared by the ToDo class needs to be initialized. In other words, before the ToDo class delegates initialization to its superclass, every property defined by the ToDo class needs to have a valid initial value. You can verify this by switching the order of the statements and inspecting the error that pops up.

The same applies to the init?(coder:) method. We first initialize the done property before invoking init?(coder:) on the superclass.
Initializers and Inheritance
When dealing with inheritance and initialization, there are a few rules to keep in mind. The rule for designated initializers is simple.
- A designated initializer needs to invoke a designated initializer from its superclass. In the
ToDoclass, for example, theinit?(coder:)method invokes theinit?(coder:)method of its superclass. This is also referred to as delegating up.
The rules for convenience initializers are a bit more complex. There are two rules to keep in mind.
- A convenience initializer always needs to invoke another initializer of the class it’s defined in. In the
Taskclass, for example, theinit()method is a convenience initializer and delegates initialization to another initializer,init(name:)in the example. This is known as delegating across. - Even though a convenience initializer doesn’t have to delegate initialization to a designated initializer, a convenience initializer needs to call a designated initializer at some point. This is necessary to fully initialize the instance that’s being initialized.
With both model classes implemented, it is time to refactor the ViewController and AddItemViewController classes. Let’s start with the latter.
2. Refactoring AddItemViewController
Step 1: Update the AddItemViewControllerDelegate Protocol
The only changes we need to make in the AddItemViewController class are related to the AddItemViewControllerDelegate protocol. In the protocol declaration, change the type of didAddItem from String to ToDo, the model class we implemented earlier.
protocol AddItemViewControllerDelegate {
func controller(_ controller: AddItemViewController, didAddItem: ToDo)
}
Step 2: Update the create(_:) Action
This means that we also need to update the create(_:) action in which we invoke the delegate method. In the updated implementation, we create a ToDo instance, passing it to the delegate method.
@IBAction func create(_ sender: Any) {
if let name = textField.text {
// Create Item
let item = ToDo(name: name, done: false)
// Notify Delegate
delegate?.controller(self, didAddItem: item)
}
}
3. Refactoring ViewController
Step 1: Update the items Property
The ViewController class requires a bit more work. We first need to change the type of the items property to [ToDo], an array of ToDo instances.
var items: [ToDo] = [] {
didSet(oldValue) {
let hasItems = items.count > 0
tableView.isHidden = !hasItems
messageLabel.isHidden = hasItems
}
}
Step 2: Table View Data Source Methods
This also means that we need to refactor a few other methods, such as the tableView(_:cellForRowAt:) method shown below. Because the items array now contains ToDo instances, checking if an item is marked as done is much simpler. We use Swift’s ternary conditional operator to update the table view cell’s accessory type.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Fetch Item
let item = items[indexPath.row]
// Dequeue Cell
let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath)
// Configure Cell
cell.textLabel?.text = item.name
cell.accessoryType = item.done ? .checkmark : .none
return cell
}
When the user deletes an item, we only need to update the items property by removing the corresponding ToDo instance. This is reflected in the implementation of the tableView(_:commit:forRowAt:) method shown below.
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
// Update Items
items.remove(at: indexPath.row)
// Update Table View
tableView.deleteRows(at: [indexPath], with: .right)
// Save State
saveItems()
}
}
Step 3: Table View Delegate Methods
Updating the state of an item when the user taps a row is handled in the tableView(_:didSelectRowAt:) method. The implementation of this UITableViewDelegate method is much simpler thanks to the ToDo class.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
// Fetch Item
let item = items[indexPath.row]
// Update Item
item.done = !item.done
// Fetch Cell
let cell = tableView.cellForRow(at: indexPath)
// Update Cell
cell?.accessoryType = item.done ? .checkmark : .none
// Save State
saveItems()
}
The corresponding ToDo instance is updated, and this change is reflected by the table view. To save the state, we invoke saveItems() instead of saveCheckedItems().
Step 4: Add Item View Controller Delegate Methods
Because we updated the AddItemViewControllerDelegate protocol, we also need to update the ViewController‘s implementation of this protocol. The change, however, is simple. We only need to update the method signature.
func controller(_ controller: AddItemViewController, didAddItem: ToDo) {
// Update Data Source
items.append(didAddItem)
// Save State
saveItems()
// Reload Table View
tableView.reloadData()
// Dismiss Add Item View Controller
dismiss(animated: true)
}
Step 5: Save Items
The pathForItems() Method
Instead of storing the items in the user defaults database, we’re going to store them in the application’s documents directory. Before we update the loadItems() and saveItems() methods, we’re going to implement a helper method named pathForItems(). The method is private and returns a path, the location of the items in the documents directory.
private func pathForItems() -> String {
guard let documentsDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first,
let url = URL(string: documentsDirectory) else {
fatalError("Documents Directory Not Found")
}
return url.appendingPathComponent("items").path
}
We first fetch the path to the documents directory in the application’s sandbox by invoking NSSearchPathForDirectoriesInDomains(_:_:_:). Because this method returns an array of strings, we grab the first item.
Notice that we use a guard statement to make sure the value returned by NSSearchPathForDirectoriesInDomains(_:_:_:) is valid. We throw a fatal error if this operation fails. This immediately terminates the application. Why do we do this? If the operating system is unable to hand us the path to the documents directory, we have bigger problems to worry about.
The value we return from pathForItems() is composed of the path to the documents directory with the string "items" appended to it.
The loadItems() Method
The loadItems method changes quite a bit. We first store the result of pathForItems() in a constant, path. We then unarchive the object archived at that path and downcast it to an optional array of ToDo instances. We use optional binding to unwrap the optional and assign it to a constant, items. In the if clause, we assign the value stored in items to the items property.
private func loadItems() {
let path = pathForItems()
if let items = NSKeyedUnarchiver.unarchiveObject(withFile: path) as? [ToDo] {
self.items = items
}
}
The saveItems() Method
The saveItems() method is short and simple. We store the result of pathForItems() in a constant, path, and invoke archiveRootObject(_:toFile:) on NSKeyedArchiver, passing in the items property and path. We print the result of the operation to the console.
private func saveItems() {
let path = pathForItems()
if NSKeyedArchiver.archiveRootObject(self.items, toFile: path) {
print("Successfully Saved")
} else {
print("Saving Failed")
}
}
Step 6: Clean Up
Let’s end with the fun part, deleting code. Start by removing the checkedItems property at the top since we no longer need it. As a result, we can also remove the loadCheckedItems() and saveCheckedItems() methods, and every reference to these methods in the ViewController class.
Build and run the application to see if everything is still working. The data model makes the application’s code much simpler and more reliable. Thanks to the ToDo class, managing the items in our list much is now easier and less error-prone.
Conclusion
In this lesson, we refactored the data model of our application. You learned more about object-oriented programming and inheritance. Instance initialization is an important concept in Swift, so make sure you understand what we’ve covered in this lesson. You can read more about initialization and initializer delegation in The Swift Programming Language.
In the meantime, check out some of our other courses and tutorials about Swift language iOS development!
SwiftCreate iOS Apps With Swift 3
iOSGo Further With Swift: Animation, Networking, and Custom Controls
SwiftCode a Side-Scrolling Game With Swift 3 and SpriteKit
iOS SDKThe Right Way to Share State Between Swift View Controllers













I love your blog.. very nice colors & theme. Did you make this website yourself or did you hire someone to do it for you? Plz reply as I’m looking to design my own blog and would like to know where u got this from. thank you
Yes, thank you for comments. We can design and build your website with a similar theme. Please contact us for further details.