Editing Table Views

One of the great features of table views is their built-in support for editing. This includes inserting new rows, deleting existing rows, and rearranging rows. In this section, you will add support for all three of those features to LootLogger.

Editing mode

UITableView has an editing property, and when this property is set to true, the UITableView enters editing mode. Once the table view is in editing mode, the rows of the table can be manipulated by the user. Depending on how the table view is configured, the user can change the order of the rows, add rows, or remove rows. (Editing mode does not allow the user to edit the content of a row.)

But first, the user needs a way to put the UITableView in editing mode. For now, you are going to include a button in the header view of the table. A header view appears at the top of a table and is useful for adding section-wide or table-wide titles and controls. It can be any UIView instance.

Note that the table view uses the word header in two different ways: There can be a table header and section headers. Likewise, there can be a table footer and section footers (Figure 9.15).

Figure 9.15  Headers and footers

Headers and footers

You are creating a table view header. It will have two subviews that are instances of UIButton: one to toggle editing mode and the other to add a new Item to the table. You could create this view programmatically, but in this case you will create the view and its subviews in the storyboard file.

First, let’s set up the necessary code. In ItemsViewController.swift, stub out two methods in the implementation.

Listing 9.13  Adding two button action methods (ItemsViewController.swift)

class ItemsViewController: UITableViewController {

    var itemStore: ItemStore!

    @IBAction func addNewItem(_ sender: UIButton) {

    }

    @IBAction func toggleEditingMode(_ sender: UIButton) {

    }

Now open Main.storyboard. From the library, drag a View to the very top of the table view, above the prototype cell. This will add the view as a header view for the table view. Resize the height of this view to be about 60 points. (You can use the size inspector if you want to make it exact.)

Now drag two Buttons from the library to the header view. Change their text and position them as shown in Figure 9.16. You do not need to be exact – you will add constraints next to position the buttons.

Figure 9.16  Adding buttons to the header view

Adding buttons to the header view

Select both of the buttons and open the Auto Layout Align menu. Select Vertically in Container with a constant of 0, and then click Add 2 Constraints (Figure 9.17).

Figure 9.17  Align menu constraints

Align menu constraints

Now open the Add New Constraints menu and configure it as shown in Figure 9.18. Make sure the values for the leading and trailing constraints save after you have typed them; sometimes the values do not save, so it can be a bit tricky. When you have done that, click Add 4 Constraints.

Figure 9.18  Adding new constraints

Adding new constraints

Finally, connect the actions for the two buttons as shown in Figure 9.19.

Figure 9.19  Connecting the two actions

Connecting the two actions

Build and run the application to see the interface.

Now let’s implement the toggleEditingMode(_:) method. You could toggle the editing property of UITableView directly. However, UIViewController also has an editing property. A UITableViewController instance automatically sets the editing property of its table view to match its own editing property. By setting the editing property on the view controller itself, you can ensure that other aspects of the interface also enter and leave editing mode. You will see an example of this in Chapter 12 with UIViewController’s editButtonItem.

To set the isEditing property for a view controller, you call the method setEditing(_:animated:). In ItemsViewController.swift, implement toggleEditingMode(_:).

Listing 9.14  Updating the interface for editing mode (ItemsViewController.swift)

@IBAction func toggleEditingMode(_ sender: UIButton) {
    // If you are currently in editing mode...
    if isEditing {
        // Change text of button to inform user of state
        sender.setTitle("Edit", for: .normal)

        // Turn off editing mode
        setEditing(false, animated: true)
    } else {
        // Change text of button to inform user of state
        sender.setTitle("Done", for: .normal)

        // Enter editing mode
        setEditing(true, animated: true)
    }
}

Build and run your application again. Tap the Edit button and the UITableView will enter editing mode (Figure 9.20).

Figure 9.20  UITableView in editing mode

UITableView in editing mode

(You might notice you can’t delete or move these rows yet. You will get to that shortly.)

Adding rows

There are two common interfaces for adding rows to a table view at runtime.

  • A button above the cells of the table view: usually for adding a record for which there is a detail view. For example, in the Contacts app, you tap a button when you meet a new person and want to take down their information.

  • A cell with a green Adding rows: usually for adding a new field to a record, such as when you want to add a birthday to a person’s record in the Contacts app. In editing mode, you tap the green Adding rows next to add birthday.

In this exercise, you will use the first option and create a New button in the header view. When this button is tapped, a new row will be added to the UITableView.

In ItemsViewController.swift, implement addNewItem(_:).

Listing 9.15  Adding a new item to the table view (ItemsViewController.swift)

@IBAction func addNewItem(_ sender: UIButton) {
    // Make a new index path for the 0th section, last row
    let lastRow = tableView.numberOfRows(inSection: 0)
    let indexPath = IndexPath(row: lastRow, section: 0)

    // Insert this new row into the table
    tableView.insertRows(at: [indexPath], with: .automatic)
}

Build and run the application. Tap the Add button and … the application crashes. The console tells you that the table view has an internal inconsistency exception.

Remember that, ultimately, it is the dataSource of the UITableView that determines the number of rows the table view should display. After inserting a new row, the table view has six rows (the original five plus the new one). When the UITableView asks its dataSource for the number of rows, the ItemsViewController consults the store and returns that there should be five rows. The UITableView cannot resolve this inconsistency and throws an exception.

You must make sure that the UITableView and its dataSource agree on the number of rows by adding a new Item to the ItemStore before inserting the new row.

In ItemsViewController.swift, update addNewItem(_:).

Listing 9.16  Fixing the crash when adding a new item (ItemsViewController.swift)

@IBAction func addNewItem(_ sender: UIButton) {
    // Make a new index path for the 0th section, last row
    let lastRow = tableView.numberOfRows(inSection: 0)
    let indexPath = IndexPath(row: lastRow, section: 0)

    // Insert this new row into the table
    tableView.insertRows(at: [indexPath], with: .automatic)

    // Create a new item and add it to the store
    let newItem = itemStore.createItem()

    // Figure out where that item is in the array
    if let index = itemStore.allItems.firstIndex(of: newItem) {
        let indexPath = IndexPath(row: index, section: 0)

        // Insert this new row into the table
        tableView.insertRows(at: [indexPath], with: .automatic)
    }
}

Let’s fix the error you are seeing where you find the index of newItem in the allItems array. The firstIndex(of:) method works by comparing the item that you are looking for to each of the items in the collection. It does this using the == operator. Types that conform to the Equatable protocol must implement this operator, and Item does not yet conform to Equatable.

In Item.swift, declare that Item conforms to the Equatable protocol and implement the required overloading of the == operator.

Listing 9.17  Defining Item equality (Item.swift)

class Item: Equatable {
    ...
    static func ==(lhs: Item, rhs: Item) -> Bool {
        return lhs.name == rhs.name
            && lhs.serialNumber == rhs.serialNumber
            && lhs.valueInDollars == rhs.valueInDollars
            && lhs.dateCreated == rhs.dateCreated
    }
}

You compare each of the properties between the two items. If all the properties match then the items are the same.

Build and run the application. Tap the Add button, and the new row will slide into the bottom position of the table. Remember that the role of a view object is to present model objects to the user; updating views without updating the model objects is not very useful.

Now that you have the ability to add rows and items, you no longer need the code that puts five random items into the store.

Open ItemStore.swift and remove the initializer code.

Listing 9.18  Removing the unneeded initializer (ItemStore.swift)

init() {
    for _ in 0..<5 {
        createItem()
    }
}

Build and run the application. There will no longer be any rows when you first launch the application, but you can add some by tapping the Add button.

Deleting rows

In editing mode, the red circles with the minus sign (shown in Figure 9.20) are deletion controls, and tapping one should delete that row. However, at this point, you cannot actually delete the row. (Try it and see.) Before the table view will delete a row, it calls a method on its data source about the proposed deletion and waits for confirmation.

When deleting a cell, you must do two things: remove the row from the UITableView and remove the Item associated with it from the ItemStore. To pull this off, the ItemStore must know how to remove objects from itself.

In ItemStore.swift, implement a new method to remove a specific item.

Listing 9.19  Removing an item from the store (ItemStore.swift)

func removeItem(_ item: Item) {
    if let index = allItems.firstIndex(of: item) {
        allItems.remove(at: index)
    }
}

Now you will implement tableView(_:commit:forRowAt:), a method from the UITableViewDataSource protocol. (This method is called on the ItemsViewController. Keep in mind that while the ItemStore is where the data is kept, the ItemsViewController is the table view’s dataSource.)

When tableView(_:commit:forRowAt:) is called on the data source, two extra arguments are passed along with it. The first is the UITableViewCell.EditingStyle, which, in this case, is .delete. The other argument is the IndexPath of the row in the table.

In ItemsViewController.swift, implement this method to have the ItemStore remove the right object and confirm the row deletion by calling the method deleteRows(at:with:) on the table view.

Listing 9.20  Implementing table view row deletion (ItemsViewController.swift)

override func tableView(_ tableView: UITableView,
                        commit editingStyle: UITableViewCell.EditingStyle,
                        forRowAt indexPath: IndexPath) {
    // If the table view is asking to commit a delete command...
    if editingStyle == .delete {
        let item = itemStore.allItems[indexPath.row]

        // Remove the item from the store
        itemStore.removeItem(item)

        // Also remove that row from the table view with an animation
        tableView.deleteRows(at: [indexPath], with: .automatic)
    }
}

Build and run your application, create some rows, and then delete a row. It will disappear. Notice that swipe-to-delete works also.

Moving rows

To change the order of rows in a UITableView, you will use another method from the UITableViewDataSource protocol – tableView(_:moveRowAt:to:).

To delete a row, you had to call the method deleteRows(at:with:) on the UITableView to confirm the deletion. Moving a row, however, does not require confirmation: The table view moves the row on its own authority and reports the move to its data source by calling the method tableView(_:moveRowAt:to:). You implement this method to update your data source to match the new order.

But before you can implement this method, you need to give the ItemStore a method to change the order of items in its allItems array.

In ItemStore.swift, implement this new method.

Listing 9.21  Reordering items within the store (ItemStore.swift)

func moveItem(from fromIndex: Int, to toIndex: Int) {
    if fromIndex == toIndex {
        return
    }

    // Get reference to object being moved so you can reinsert it
    let movedItem = allItems[fromIndex]

    // Remove item from array
    allItems.remove(at: fromIndex)

    // Insert item in array at new location
    allItems.insert(movedItem, at: toIndex)
}

In ItemsViewController.swift, implement tableView(_:moveRowAt:to:) to update the store.

Listing 9.22  Implementing table view row reordering (ItemsViewController.swift)

override func tableView(_ tableView: UITableView,
                        moveRowAt sourceIndexPath: IndexPath,
                        to destinationIndexPath: IndexPath) {
    // Update the model
    itemStore.moveItem(from: sourceIndexPath.row, to: destinationIndexPath.row)
}

Build and run your application. Add a few items, then tap Edit and check out the new reordering controls (the three horizontal lines) on the side of each row. Touch and hold a reordering control and move the row to a new position (Figure 9.21).

Figure 9.21  Moving a row

Moving a row

Note that simply implementing tableView(_:moveRowAt:to:) caused the reordering controls to appear. The UITableView asks its data source at runtime whether it implements tableView(_:moveRowAt:to:). If it does, then the table view adds the reordering controls whenever the table view enters editing mode.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset