@Husain posted a great write-up for his solutions to the Bronze, Silver and Gold challenges. They work and I learned a lot from reading them. However, this part of his Gold implementation kind of bugged me because it didn’t feel like the way other apps delete things on my phone.
// GOLD CHALLENGE: required function
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
todoList.remove(indexPath.row)
itemTextField.text = ""
tableView.reloadData()
}
When I implemented the Gold Solution in a similar way the item just popped off the screen the moment I touched it. So I wanted to add another way to solve the Gold Challenge that takes full advantage of the animated iOS features to confirm delete and animate the removal. This is my first book on Swift or iOS so it took a day of digging through the documentation. After all that effort I thought I would share what I learned and write a short tutorial on another way to solve the Gold Challenge that puts the UITableView
into edit mode and the user clicks a new Done button to return to insert mode.
So the first thing we will need is another button, the doneButton
, added to the storyboard. Put it right on top of the insertButton
with all of the same constraints the chapter dictates for the insert button. The new Done button and the existing Insert button will have to be accessible in code to toggle their isHidden
states depending on the state of the app. So add this code to top of the ViewController:
@IBOutlet var insertButton: UIButton!
@IBOutlet var doneButton: UIButton!
When the Done button is pressed we will need to catch that press in the View Controller so add a stub for the @IBAction
to the bottom of the ViewController
class like this:
@IBAction func doneButtonPressed(_ sender: UIButton){
print("Done button pressed")
}
Then in the storyboard link the Done button to the IBAction
as described for the Insert button in the chapter.
Next the View Controller needs a few private methods to control the state of all the controls. So add these private functions:
private func configureControlsForEditMode() {
itemTextField.isHidden = true
insertButton.isHidden = true
doneButton.isHidden = false
tableView.setEditing(true, animated: true)
}
private func configureControlsForInsertMode() {
itemTextField.isHidden = false
insertButton.isHidden = false
doneButton.isHidden = true
tableView.setEditing(false, animated: true)
}
The new part here is the tableView.setEditing
method call. This simple call puts the familiar red circle with the minus sign next to all of the existing entries. Then when you touch the minus, the item will slide left and expose a delete confirmation. I use these private methods to control the state of all the app controls. This first happens when the view is first set up in the viewDidLoad()
method.
override func viewDidLoad() {
super.viewDidLoad()
//Silver challenge - have the todoList provide its cell type and cell identifier to better support
//the MVC design pattern.
let cellInfo = todoList.cellInfo
tableView.register(cellInfo.cellClass, forCellReuseIdentifier: cellInfo.cellIdentifier)
tableView.dataSource = todoList
//Gold Challenge - register self as the UITableViewDelegate
tableView.delegate = self
configureControlsForInsertMode()
}
The only remaining changes in the View Controller are filling out the doneButtonPressed
method and catching that a row was selected like this:
@IBAction func doneButtonPressed(_ sender: UIButton){
itemTextField.text = ""
configureControlsForInsertMode()
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
configureControlsForEditMode()
}
After building and running with these changes you should see the animation provided by the UITableView
class to shift from edit mode and back when you press Done. Next, I’ll describe my changes in the TodoList.swift
file to deal with the data model for the app.
One minor note of difference from my Silver Challenge solution and the one posted by @Husain is that I particularly like the getter/setter syntax provided for properties in Swift and chose to use that syntax as the mechanism for the model to send configuration data back to the view controller.
//Silver Challenge - provide a tuple for the View Controller to get cell info from the model
//without a priori knowledge of its desired class and identifier
var cellInfo: (cellClass: AnyClass, cellIdentifier: String ) {
get {
return (UITableViewCell.self, "Cell")
}
}
Similar to others, I also added a remove function to the TodoList
class:
func remove(at index: Int) {
items.remove(at: index)
saveItems()
}
The major change in my solution from the one already posted is in the extension on TodoList to use another delegate capability of UITableView
.
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath){
if editingStyle == .delete {
remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: UITableViewRowAnimation.automatic)
}
}
If you take a look at this function in the reference documentation you will find that the tableView
instance in the View Controller sends this signal to the model after the user selects the delete option in the view. By catching this signal and then calling the models remove(at:)
method we can take action on the model data. Once the model is updated we can return a signal to the View Controller to animate the deletion sequence using the tableView.deleteRows(at:with)
method.
This technique of catching signals in the view controller, sending them to the model to deal with the data and then signalling back to the view controller to provide a clean animated sequence is pretty neat, and I think it is in the spirit of the lesson.
After slogging through 410 pages of Swift syntax rules I was glad to be able to build a little bit of functionality into this app that felt like the “iOS way”.
Full code listing for my View Controller:
import UIKit
class ViewController: UIViewController, UITableViewDelegate {
@IBOutlet var itemTextField: UITextField!
@IBOutlet var tableView: UITableView!
@IBOutlet var insertButton: UIButton!
@IBOutlet var doneButton: UIButton!
let todoList = TodoList()
private func configureControlsForEditMode() {
itemTextField.isHidden = true
insertButton.isHidden = true
doneButton.isHidden = false
tableView.setEditing(true, animated: true)
}
private func configureControlsForInsertMode() {
itemTextField.isHidden = false
insertButton.isHidden = false
doneButton.isHidden = true
tableView.setEditing(false, animated: true)
}
override func viewDidLoad() {
super.viewDidLoad()
//Silver challenge - have the todoList provide its cell type and cell identifier to better support
//the MVC design pattern.
let cellInfo = todoList.cellInfo
tableView.register(cellInfo.cellClass, forCellReuseIdentifier: cellInfo.cellIdentifier)
tableView.dataSource = todoList
//Gold Challenge - register self as the UITableViewDelegate
tableView.delegate = self
configureControlsForInsertMode()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
@IBAction func insertButtonPressed(_ sender: UIButton){
guard let todo = itemTextField.text else {
return
}
//Bronze challenge to only insert rows when there is content in the text field
if !todo.isEmpty {
todoList.add(todo)
}
tableView.reloadData()
//Bronze challenge to clear the text field after Insert is pressed
itemTextField.text = ""
}
@IBAction func doneButtonPressed(_ sender: UIButton){
itemTextField.text = ""
configureControlsForInsertMode()
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
configureControlsForEditMode()
}
}
Full code listing for my TodoList Class.
import UIKit
class TodoList: NSObject {
private let fileURL: URL = {
let documentDirectoryURLs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let documentDirectoryURL = documentDirectoryURLs.first!
return documentDirectoryURL.appendingPathComponent("todolist.items")
}()
fileprivate var items: [String] = []
//Silver Challenge - provide a tuple for the View Controller to get cell info from the model
//without a priori knowledge of its desired class and identifier
var cellInfo: (cellClass: AnyClass, cellIdentifier: String ) {
get {
return (UITableViewCell.self, "Cell")
}
}
override init() {
super.init()
loadItems()
}
func saveItems() {
let itemsArray = items as NSArray
print("Saving items to \(fileURL)")
if !itemsArray.write(to: fileURL, atomically: true) {
print("Could not save to-do list")
}
}
func loadItems() {
if let itemsArray = NSArray(contentsOf: fileURL) as? [String] {
items = itemsArray
}
}
func add(_ item: String){
items.append(item)
saveItems()
}
func remove(at index: Int) {
items.remove(at: index)
saveItems()
}
}
extension TodoList: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let item = items[indexPath.row]
cell.textLabel!.text = item
return cell
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath){
if editingStyle == .delete {
remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: UITableViewRowAnimation.automatic)
}
}
}