Advertisement
  1. Code
  2. Swift
Code

Swift From Scratch: Initialization and Initializer Delegation

by
Difficulty:IntermediateLength:LongLanguages:
This post is part of a series called Swift From Scratch.
Swift From Scratch: Access Control and Property Observers

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.

Create A Data Model Class

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

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.

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.

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.

Initialization Error

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

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.

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.

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.

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.

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.

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

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.

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.

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!

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.