Blog

Rearranging and hiding items in a UITableView

Creating a TableView just like Apple's own Mail App where you rearrange and hide Mail Folders is pretty straigthforward, once you know how to do it.

Initial Table

After searching blogs, iOS documentation and Stack Overflow I finally found the (a?) proper way.
In this blog I share my findings. I will build a table that can be rearranged and where you can select rows (to hide them).

Part 1 - Rearranging items

Create a Simple TableView first

Create a (Swift) XCode project first with one UITableViewController. Create a UITableViewController inside a UINavigationController and connect it to InterfaceBuilder.
Or check out MultiSelectTableView on Github. There are tags for each step in the repo. Run the App and verify everything is in order.

Let's add some content.

class MultiSelectTableViewController: UITableViewController {

  let toDos = ["Go biking", "Italian Groceries", "Fix heating", "Install dimmer", "Pick up books"]

  override func viewDidLoad() {
    super.viewDidLoad()
  }

  override func numberOfSections(in tableView: UITableView) -> Int {
    return 1
  }

  override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return toDos.count
  }

  override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)

    cell.textLabel?.text = self.toDos[indexPath.row]

    return cell
  }
}

Initial Table

Make the table editable

With just one line of code you can make this work, no need to use InterfaceBuilder. Add the following line to your viewDidLoad:

self.navigationItem.rightBarButtonItem = self.editButtonItem

Hmm great, we can put the table in edit mode. But I don't want to delete items. Just reorder them. Let's fix this:

override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle {
  return UITableViewCellEditingStyle.none
}

Reorder items in the table

In order to reorder the items you only need to override two methods:

override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
  return true
}

override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
}

Now in edit mode you can rearrange your Todo list:


Reorder Table

Store the order of the items

We can now reorder the items, but we need to persist them between views. We can use UserDefaults to do that:

let toDoStore = UserDefaults(suiteName: "MultiSelectTable")
var toDos = [String]()

func initializeToDoList() {
  if let defaults = self.toDoStore?.array(forKey: "toDos") as? [String] {
    self.toDos = defaults
  } else {
    self.toDos = ["Go biking", "Italian Groceries", "Fix heating", "Install dimmer", "Pick up books"]
    self.toDoStore?.set(self.toDos, forKey: "toDos")
  }
}

And initialize in viewDidLoad for example. The only thing left to do is to store the changes when we move items around:

override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
  let todo = self.toDos.remove(at: sourceIndexPath.row)
  self.toDos.insert(todo, at: destinationIndexPath.row)
  self.toDoStore?.set(self.toDos, forKey: "toDos")
}

Part 2 - Hiding items

Make items selectable

This is easy again, just add:
swift
self.tableView.allowsMultipleSelectionDuringEditing = true

to your viewDidLoad.

But no items are selected, and by default we want to select everything. We need to tell our cell to do so:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)

  cell.textLabel?.text = self.toDos[indexPath.row]
  if self.isEditing {
    self.tableView.selectRow(at: indexPath, animated: false, scrollPosition: UITableViewScrollPosition.none)
  }

  return cell
}

Almost there. We need to tell the table to redraw itself once it goes into editing mode. Let's override the default behavior:

override func setEditing(_ editing: Bool, animated: Bool) {
  super.setEditing(editing, animated: animated)
  tableView.setEditing(editing, animated: true)

  self.tableView.reloadData()
}

And get rid of the ugly background color while we're at it:

    let bgColorView = UIView()
    bgColorView.backgroundColor = UIColor.white
    cell.selectedBackgroundView = bgColorView

Reorder Table

Store the selection

Every time we select or deselect an item, we need to store this. Let's just store all items we want to hide.

var hiddenToDos = Set<String>()

First deal with (de)selection of items:

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  self.hiddenToDos.remove(self.toDos[indexPath.row])
}

override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
  self.hiddenToDos.insert(self.toDos[indexPath.row])
}

And hide them when we're done editing

Unless editing, we need to hide the items from the tableView, else we need to show all items with their selection:

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  var nrOfRows = self.toDos.count
  if !self.isEditing {
    nrOfRows -= self.hiddenToDos.count
  }
  return nrOfRows
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)

  if self.isEditing {
    cell.textLabel?.text = self.toDos[indexPath.row]
    if !self.hiddenToDos.contains(self.toDos[indexPath.row]) {
      self.tableView.selectRow(at: indexPath, animated: false, scrollPosition: UITableViewScrollPosition.none)
    }
    let bgColorView = UIView()
    bgColorView.backgroundColor = UIColor.white
    cell.selectedBackgroundView = bgColorView
  } else {
    let todosWithHiddenItemsSkipped = self.toDos.filter { todo in
      !self.hiddenToDos.contains(todo)
    }
    cell.textLabel?.text = todosWithHiddenItemsSkipped[indexPath.row]
  }

  return cell
}

At last persist the hidden items

At initialization add:

if let hiddenToDoFromStore = self.toDoStore?.array(forKey: "hidden") as? [String] {
  self.hiddenToDos = Set<String>(hiddenToDoFromStore)
}

And when we change selection update the store:

self.toDoStore?.set(Array(self.hiddenToDos), forKey: "hidden")

And we're done!