Todo App - iOS

The todo application uses Suas to add, remove and move a list of todo items. In our Todo app, the list of todos is represented as a TodoListView view. This todo list contains a table view. (Check the Todo application source code on GitHub)

Below is the view used in our todo application.

class TodoListView: UIView, Component {
  let tableView = UITableView()

  var state: TodoList = TodoReducer().initialState {
    didSet {
      // When state changes, we reload the table
      tableView.reloadData()
    }
  }

  convenience init() {
    self.init(frame: CGRect(x: 0, y: 0, width: 300, height: 500))
    addSubview(tableView)
    tableView.delegate = self
    tableView.dataSource = self
    tableView.frame = frame
    
    // Add a listener to the store
    // Add store. [Weak self] to prevent retain cycles
    store.addListener(forStateType: TodoList.self) { [weak self] newState in
      // Notice that we capture self weakly to prevent strong memory cycles
      self?.state = newState
    }.linkLifeCycleTo(object: self)
  }
}

extension TodoListView: UITableViewDataSource, UITableViewDelegate {
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    // the number of cells is the number of todos
    return state.todos.count
  }

  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = UITableViewCell(style: .default, reuseIdentifier: "TodoCell")

    // Use the states to populate the cell
    let todoItem = state.todos[indexPath.row]
    cell.textLabel?.text = todoItem.title
    cell.textLabel?.textColor = todoItem.isCompleted ? UIColor.gray : UIColor.black
    return cell
  }

  func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    // Toggle a cell completion status by dispatching actions
    store.dispatch(action: ToggleTodo(index: indexPath.row))
  }
}

First, our TodoListView component defines its state of TodoList type.

var state: TodoList = TodoReducer().initialState {
  didSet {
    // When state changes, we reload the table
    tableView.reloadData()
  }
}

In the init function, the component adds a listener to the state by calling store.addListener. When the TodoList state in the store changes, the store notifies this listener.

When the listener is notified, we update the state in the TodoListView which invalidates the table so that it reads the new todo elements from the state.

In the UITableView data source functions we use the view's state to populate the view:

  • We use the count of the todo to specify how many cells the table has
  • The todo item title and completion status is used to populate the cell
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    // the number of cells is the number of todos
    return state.todos.count
  }

  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = UITableViewCell(style: .default, reuseIdentifier: "TodoCell")

    // Use the states to populate the cell
    let todoItem = state.todos[indexPath.row]
    cell.textLabel?.text = todoItem.title
    cell.textLabel?.textColor = todoItem.isCompleted ? UIColor.gray : UIColor.black
    return cell
  }

In our todo app, we want to mark an item as completed when we tap on that todo item cell. To achieve that we dispatch the ToggleTodo action to the store.

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    // When state changes, we reload the table
    store.dispatch(action: ToggleTodo(index: indexPath.row))
  }

What's Next

Todo application source code on GitHub

Learn more about Suas listeners

Suas listeners
Using the StateSelector
Adding a listener with a filter

Other sample apps

List of sample applications
Counter App Example
Todo app with settings example
Search Cities Example