In the previous lesson, we added the ability to create to-do items. While this addition has made the application a bit more useful, it would also be convenient to add the ability to mark items as done and delete items. That’s what we’ll focus on in this lesson.
Prerequisites
If you’d like to follow along with me, then make sure that you have Xcode 8.3.2 or higher installed on your machine. You can download Xcode 8.3.2 from Apple’s App Store.
1. Deleting Items
To delete items, we need to implement two additional methods of the UITableViewDataSource
protocol. We first need to tell the table view which rows can be edited by implementing the tableView(_:canEditRowAt:)
method. As you can see in the below code snippet, the implementation is straightforward. We tell the table view that every row is editable by returning true
.
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { return true }
The second method we’re interested in is tableView(_:commit:forRowAt:)
. The implementation is a bit more complex but easy enough to grasp.
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) } }
We start by checking the value of editingStyle
, an enumeration of type UITableViewCellEditingStyle
. We only delete an item if the value of editingStyle
is equal to UITableViewCellEditingStyle.delete
.
Swift is smarter than that, though. Because it knows that editingStyle
is of type UITableViewCellEditingStyle
, we can omit UITableViewCellEditingStyle
, the name of the enumeration, and write .delete
, the member value of the enumeration that we’re interested in. If you’re new to enumerations in Swift, then I recommend you read this quick tip about enumerations in Swift.
Next, we update the table view’s data source, items
, by invoking remove(at:)
on the items
property, passing in the correct index. We also update the table view by invoking deleteRows(at:with:)
on tableView
, passing in an array with indexPath
and .right
to specify the animation type. As we saw earlier, we can omit the name of the enumeration, UITableViewRowAnimation
, since Swift knows the type of the second argument is UITableViewRowAnimation
.
The user should now be able to delete items from the list. Build and run the application to test this.
2. Checking Off Items
To mark an item as done, we’re going to add a checkmark to the corresponding row. This implies that we need to keep track of the items the user has marked as done. For that purpose, we’ll declare a new property that manages this for us. Declare a variable property, checkedItems
, of type [String]
, and initialize it with an empty array.
var checkedItems: [String] = []
In tableView(_:cellForRowAt:)
, we check whether checkedItems
contains the respective item by invoking the contains(_:)
method, passing in the item that corresponds with the current row. The method returns true
if checkedItems
contains item
.
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 if checkedItems.contains(item) { cell.accessoryType = .checkmark } else { cell.accessoryType = .none } return cell }
If item
is found in checkedItems
, we set the cell’s accessoryType
property to .checkmark
, a member value of the UITableViewCellAccessoryType
enumeration. If item
isn’t found, we fall back to .none
as the cell’s accessory type.
The next step is adding the ability to mark an item as done by implementing a method of the UITableViewDelegate
protocol, tableView(_:didSelectRowAt:)
. In this delegate method, we first call deselectRow(at:animated:)
on tableView
to deselect the row the user tapped.
// MARK: - Table View Delegate Methods func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) // Fetch Item let item = items[indexPath.row] // Fetch Cell let cell = tableView.cellForRow(at: indexPath) // Find Index of Item let index = checkedItems.index(of: item) if let index = index { checkedItems.remove(at: index) cell?.accessoryType = .none } else { checkedItems.append(item) cell?.accessoryType = .checkmark } }
We then fetch the corresponding item from items
and a reference to the cell that corresponds with the tapped row. We ask checkedItems
for the index of the corresponding item by invoking index(of:)
. This method returns an optional Int
. If checkedItems
contains item
, we remove it from checkedItems
and set the cell’s accessory type to .none
. If checkedItems
doesn’t contain item
, we add it to checkedItems
and set the cell’s accessory type to .checkmark
.
With these additions, the user is now able to mark items as done. Build and run the application to make sure that everything is working as expected.
3. Saving State
The application currently doesn’t save state between launches. To solve this, we’re going to store the items
and checkedItems
arrays in the application’s user defaults database.
Step 1: Loading State
Start by creating two helper methods, loadItems()
and loadCheckedItems()
. Note the private
keyword prefixing each helper method. The private
keyword tells Swift that these methods are only accessible from within the ViewController
class.
// MARK: Private Helper Methods private func loadItems() { let userDefaults = UserDefaults.standard if let items = userDefaults.object(forKey: "items") as? [String] { self.items = items } } private func loadCheckedItems() { let userDefaults = UserDefaults.standard if let checkedItems = userDefaults.object(forKey: "checkedItems") as? [String] { self.checkedItems = checkedItems } }
The private
keyword is part of Swift’s access control. As the name implies, access control defines which code has access to which code. Access levels apply to methods, functions, types, etc. Apple simply refers to entities. There are five access levels: open, public, internal, file-private, and private.
- Open/Public: Entities marked as open or public are accessible by entities defined in the same module as well as other modules. This is ideal for exposing the interface of a framework. There are several differences between the open and public access levels. You can read more about these differences in The Swift Programming Language.
- Internal: This is the default access level. In other words, if no access level is specified, this access level applies. An entity with an access level of internal is only accessible by entities defined in the same module.
-
File-Private: An entity declared as file-private is only accessible by entities defined in the same source file. For example, the private helper methods defined in the
ViewController
class are only accessible by theViewController
class. -
Private: Private is very similar to file-private. The only difference is that an entity declared as private is only accessible from within the declaration it is enclosed by. For example, if we create an extension for the
ViewController
class in ViewController.swift, any entities that are marked as file-private would not be accessible in the extension, but private entities would be accessible.
The implementation of the helper methods is simple if you’re familiar with the UserDefaults
class. For ease of use, we store a reference to the standard user defaults object in a constant named userDefaults
. In the case of loadItems()
, we ask userDefaults
for the object associated with the key "items"
and downcast it to an optional array of strings. We safely unwrap the optional, which means that we store the value in the constant items
if the optional is not nil
, and assign the value to the items
property of the view controller.
If the if
statement looks confusing, then have a look at a simpler version of the loadItems()
method in the following example. The result is identical; the only difference is conciseness.
private func loadItems() { let userDefaults = UserDefaults.standard let storedItems = userDefaults.object(forKey: "items") as? [String] if let items = storedItems { self.items = items } }
The implementation of loadCheckedItems()
is identical except for the key used to load the object stored in the user defaults database. Let’s put loadItems()
and loadCheckedItems()
to use by updating the viewDidLoad()
method.
override func viewDidLoad() { super.viewDidLoad() // Set Title title = "To Do" // Populate Items items = ["Buy Milk", "Finish Tutorial", "Play Minecraft"] // Load State loadItems() loadCheckedItems() // Register Class for Cell Reuse tableView.register(UITableViewCell.self, forCellReuseIdentifier: "TableViewCell") }
Step 2: Saving State
To save state, we implement two more private helper methods, saveItems()
and saveCheckedItems()
. The logic is similar to that of loadItems()
and loadCheckedItems()
. The difference is that we store data in the user defaults database. Make sure that the keys used in the setObject(_:forKey:)
calls match those used in loadItems()
and loadCheckedItems()
.
private func saveItems() { let userDefaults = UserDefaults.standard // Update User Defaults userDefaults.set(items, forKey: "items") userDefaults.synchronize() } private func saveCheckedItems() { let userDefaults = UserDefaults.standard // Update User Defaults userDefaults.set(checkedItems, forKey: "checkedItems") userDefaults.synchronize() }
The synchronize()
call isn’t strictly necessary. The operating system will make sure that the data you store in the user defaults database is written to disk at some point. By invoking synchronize()
, however, you explicitly tell the operating system to write any pending changes to disk. This is useful during development, because the operating system won’t write your changes to disk if you kill the application. It may then seem as if something isn’t working properly.
We need to invoke saveItems()
and saveCheckedItems()
in a number of places. To start, call saveItems()
when a new item is added to the list. We do this in the delegate method of the AddItemViewControllerDelegate
protocol.
// MARK: Add Item View Controller Delegate Methods func controller(_ controller: AddItemViewController, didAddItem: String) { // Update Data Source items.append(didAddItem) // Save State saveItems() // Reload Table View tableView.reloadData() // Dismiss Add Item View Controller dismiss(animated: true) }
When the state of an item changes in the tableView(_:didSelectRowAt:)
, we update checkedItems
. It’s a good idea to also invoke saveCheckedItems()
at that point.
// MARK: - Table View Delegate Methods func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) // Fetch Item let item = items[indexPath.row] // Fetch Cell let cell = tableView.cellForRow(at: indexPath) // Find Index of Item let index = checkedItems.index(of: item) if let index = index { checkedItems.remove(at: index) cell?.accessoryType = .none } else { checkedItems.append(item) cell?.accessoryType = .checkmark } // Save State saveCheckedItems() }
When an item is deleted, both items
and checkedItems
are updated. To save this change, we call both saveItems()
and saveCheckedItems()
.
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { if editingStyle == .delete { // Fetch Item let item = items[indexPath.row] // Update Items items.remove(at: indexPath.row) if let index = checkedItems.index(of: item) { checkedItems.remove(at: index) } // Update Table View tableView.deleteRows(at: [indexPath], with: .right) // Save State saveItems() saveCheckedItems() } }
That’s it. Build and run the application to test your work. Play with the application and force quit it. When you launch the application again, the last known state should be loaded and visible.
4. Property Observers
The application’s user experience is a bit lacking at the moment. When every item is deleted or when the application is launched for the first time, the user sees an empty table view. This isn’t great. We can solve this by showing a message when there are no items. This will also give me the opportunity to show you another feature of Swift, property observers.
Step 1: Adding a Label
Let’s start by adding a label to the user interface for showing the message. Declare an outlet named messageLabel
of type UILabel
in the ViewController
class, open Main.storyboard, and add a label to the view controller’s view.
@IBOutlet var messageLabel: UILabel!
Add the necessary layout constraints to the label and connect it with the view controller’s messageLabel
outlet in the Connections Inspector. Set the label’s text to You don’t have any to-dos. and center the label’s text in the Attributes Inspector.
Step 2: Implementing a Property Observer
The message label should only be visible if items
contains no elements. When that happens, we should also hide the table view. We could solve this problem by adding various checks in the ViewController
class, but a more convenient and elegant approach is to use a property observer.
As the name implies, property observers observe a property. A property observer is invoked whenever a property changes, even when the new value is the same as the old value. There are two types of property observers.
-
willSet
: invoked before the value has changed -
didSet
: invoked after the value has changed
For our purpose, we will implement the didSet
observer for the items
property. Take a look at the syntax in the following code snippet.
var items: [String] = [] { didSet { let hasItems = items.count > 0 tableView.isHidden = !hasItems messageLabel.isHidden = hasItems } }
The construct may look a bit odd at first, so let me explain what’s happening. When the didSet
property observer is invoked, after the items
property has changed, we check if the items
property contains any elements. Based on the value of the hasItems
constant, we update the user interface. It’s as simple as that.
The didSet
observer is passed a constant parameter that contains the value of the old value of the property. It is omitted in the above example, because we don’t need it in our implementation. The following example shows how it could be used.
var items: [String] = [] { didSet(oldValue) { if oldValue != items { let hasItems = items.count > 0 tableView.isHidden = !hasItems messageLabel.isHidden = hasItems } } }
The oldValue
parameter in the example doesn’t have an explicit type, because Swift knows the type of the items
property. In the example, we only update the user interface if the old value differs from the new value.
A willSet
observer works in a similar fashion. The main difference is that the parameter passed to the willSet
observer is a constant holding the new value of the property. When using property observers, keep in mind that they are not invoked when the instance is initialized.
Build and run the application to make sure everything is hooked up correctly. Even though the application isn’t perfect and could use a few more features, you have created your first iOS application using Swift.
Conclusion
Over the course of the last three lessons of this series, you created a functional iOS application using Swift’s object-oriented features. If you have some experience programming and developing applications, then you must have noticed that the current data model has a few shortcomings, to put it lightly.
Storing items as strings and creating a separate array to store an item’s state isn’t a good idea if you’re building a proper application. A better approach would be to create a separate ToDo
class for modeling items and store them in the application’s sandbox. That will be our goal for the next lesson of this series.
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
Powered by WPeMatico