Swift From Scratch: Initialization and Initializer Delegation

In the previous installments of Swift from Scratch, we created a functional to-do application. The data model could use some love though. In this final tutorial, we're going to refactor the data model by implementing a custom model class.

1. 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 continue our exploration of object-oriented programming in Swift. In this tutorial, we zoom in on the initialization of class instances and what role inheritance plays during initialization.

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.

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 is 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.

NSCoding Protocol

The implementation of the Task class isn't complete though. Later in this article, 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 form 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.

Next, we need to implement the two methods declared in the NSCoding protocol, init(coder:) and encodeWithCoder(_:). The implementation is straightforward if you're familiar with the NSCoding protocol.

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 encodeWithCoder(_:), 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 encodeWithCoder(_:) 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.

ToDo Class

With the Task class implemented, it's time to implement the ToDo class. Create a new Swift file and name is ToDo.swift. Let's look at the implementation of the ToDo class.

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 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 the ToDo class declares, needs to have a valid initial value. You can verify this by switching the order of the statements and inspect 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. Also note that we downcast and force unwrap the result of decodeObjectForKey(_:) to a Bool using as!.

Initializers and Inheritance

When dealing with inheritance and initialization, there are a few rules to keep in mind. The rule for designated initializers are simple.

  • A designated initializer needs to invoke a designated initializer from its superclass. In the ToDo class, for example, the init(coder:) method invokes the init(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 Task class, for example, the init 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: Updating 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.

Step 2: Update 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.

3. Refactoring ViewController

Step 1: Updating 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.

Step 2: Table View Data Source Methods

This also means that we need to refactor a few other methods, such as the cellForRowAtIndexPath(_:) 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.

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(_:commitEditingStyle:forRowAtIndexPath:) method shown below.

Step 3: Table View Delegate Methods

Updating the state of an item when the user taps a row is handled in the tableView(_:didSelectRowAtIndexPath:) method. The implementation of this UITableViewDelegate method is much simpler thanks to the ToDo class.

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.

Step 5: Saving Items

pathForItems

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.

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, force unwrap it, and downcast it to a String. The value we return from pathForItems is composed of the path to the documents directory with the string "items" appended to it.

loadItems

The loadItems method changes quite a bit. We first store the result of pathForItems in a constant named 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 named items. In the if clause, we assign the value stored in items to the items property.

saveItems

The saveItems method is short and simple. We store the result of pathForItems in a constant named path and invoke archiveRootObject(_:toFile:) on NSKeyedArchiver, passing in the items property and path. We print the result of the operation to the console.

Step 6: Cleaning 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 reliable. Thanks to the ToDo class, managing the items in our list much is now easier and less error-prone.

Conclusion

In this final tutorial of the series, 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 tutorial. You can read more about initialization and initializer delegation in The Swift Programming Language.

Congratulations on completing the Swift From Scratch series. You've now got a good base to build on as you continue your journey to becoming an expert Swift app developer!

Tags:

Comments

Related Articles