Bronze, Silver and Gold Challenges - Another Way

@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)
        }
    }
}

@zacsketches thanks for posting your solution, and glad that I could help!

The reason I merely used a tap to remove the item is because that’s what the book asks us to do. The Gold Challenge demands that we give the user the ability to “remove an item by tapping on it.” Since I am novice in the field of iOS development, I was trying to get the simple things right at first.

But it’s great to see you looking ahead to improve your app! I will try to come back to your solution and learn from it whenever I have the chance.

Yours,
Husain