Skip to content

Latest commit

 

History

History
478 lines (364 loc) · 12.3 KB

demo.md

File metadata and controls

478 lines (364 loc) · 12.3 KB

Present the project structure, build & run as empty.

Then start with the state

/// Current controller state
enum State {
  case empty
  case data(Results<Task>)
  case error(Error)
}

private var state: State = .empty {
  didSet {
    updateState()
  }
}

private lazy var placeholderView: PlaceholderView = { return PlaceholderView.loadFromNib() }()
  /// Update listing state
  private func updateState() {

    var plState: PlaceholderView.State?

    switch state {
    case .empty:
      plState = PlaceholderView.State(emotion: "🦃",
                                      title: L10n.listingEmptyTitle,
                                      details: L10n.listingEmptyDescription)
    case .data:
      break
    case .error(let error):
      plState = PlaceholderView.State(emotion: "😣",
                                      title: L10n.listingErrorTitle,
                                      details: error.localizedDescription)
    }

    if let plState = plState {
      placeholderView.state = plState
      placeholderView.frame = tableView.bounds
      tableView.tableHeaderView = placeholderView
      tableView.isScrollEnabled = false
    } else {
      tableView.tableHeaderView = nil
      tableView.isScrollEnabled = true
    }

  }

Then build & run

in viewDidLoad / refresh

let tasks = Realm.ex.safeInstance().objects(Task.self).sorted(byKeyPath: #keyPath(Task.dateCreated),
                                                                  ascending: false)
state = tasks.isEmpty ? .empty : .data(tasks)
tableView.register(cellType: TaskCell.self)

DataSource

  override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    guard case .data(let tasks) = state else { return 0 }

    return tasks.count
  }

  override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    guard case .data(let tasks) = state else { fatalError("Cannot fill any cell without data") }

    let cell: TaskCell = tableView.dequeueReusableCell(for: indexPath)

    let task = tasks[indexPath.row]
    cell.infoLabel.text = task.name

    return cell
  }

UITableViewDelegate

  override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    guard case .data(let tasks) = state else { return }

    let task = tasks[indexPath.row]

    let details = TaskDetailViewController.make(task: task)
    let navVC = UINavigationController(rootViewController: details)

    splitViewController?.showDetailViewController(navVC, sender: self)
  }

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

  override func tableView(_ tableView: UITableView,
                          commit editingStyle: UITableViewCellEditingStyle,
                          forRowAt indexPath: IndexPath) {

    guard case .data(let tasks) = state else { return }

    if editingStyle == .delete {
      let task = tasks[indexPath.row]
      try? task.delete()
    }
  }

Then build & run

Add task

let addButton = UIBarButtonItem(barButtonSystemItem: .add,
                                    target: self,
                                    action: #selector(insertNewObject(_:)))
navigationItem.rightBarButtonItem = addButton
  @objc
  func insertNewObject(_ sender: Any) {

    let alert = UIAlertController(title: L10n.taskAddTitle,
                                  message: nil,
                                  preferredStyle: .alert)
    alert.addTextField { textField in
      textField.placeholder = L10n.taskNamePlaceholder
    }
    alert.addAction(UIAlertAction(title: L10n.commonCancel, style: .cancel, handler: nil))
    alert.addAction(UIAlertAction(title: L10n.commonAdd, style: .default, handler: { _ in

      let name = alert.textFields?.first?.text ?? ""

      //create the task

    }))

    self.present(alert, animated: true, completion: nil)
  }
do {
  try Task.add(name: name)
} catch {
  self.state = .error(error)
}
  tasksToken = tasks.observe { changes in

      switch changes {
      case .initial(let values):
        self.state = values.isEmpty ? .empty : .data(values)
        self.tableView.reloadData()

      case .update(let values, let deletions, let insertions, let modifications):
        self.state = values.isEmpty ? .empty : .data(values)
        self.tableView.beginUpdates()
        self.tableView.deleteRows(at: deletions.compactMap { IndexPath(row: $0, section: 0) }, with: .automatic)
        self.tableView.insertRows(at: insertions.compactMap { IndexPath(row: $0, section: 0) }, with: .automatic)
        self.tableView.reloadRows(at: modifications.compactMap { IndexPath(row: $0, section: 0) }, with: .automatic)
        self.tableView.endUpdates()

      case .error(let error):
        log.error("An error occured during update: \(error)")
        self.state = .error(error)
      }

  }
private var tasksToken: NotificationToken?
deinit {
  tasksToken?.invalidate()
  tasksToken = nil
}

Enrich the cell

  let currentFont = cell.infoLabel.font ?? UIFont.preferredFont(forTextStyle: .body)
  let striketroughStyle: NSUnderlineStyle = task.completed ? .styleThick : .styleNone
  let text = NSMutableAttributedString(string: task.name ?? "",
                                         attributes: [.strikethroughStyle: striketroughStyle.rawValue])

  cell.completedButton.isSelected = task.completed
  cell.completedButton.addTarget(self, action: #selector(toggleTaskState(_:)), for: .touchUpInside)
  cell.infoLabel.attributedText = text
  @objc
  private func toggleTaskState(_ sender: UIButton) {

    guard case .data(let tasks) = state else { return }

    let center = tableView.convert(sender.center, from: sender.superview)
    guard let idx = tableView.indexPathForRow(at: center) else { return }

    let task = tasks[idx.row]

    do {
      let realm = Realm.ex.safeInstance()
      try realm.write {
        task.completed = !sender.isSelected
      }
    } catch {
      state = .error(error)
    }

  }

Then build & run | demo the add / remove etc... cool !

cell.textfield.addTarget(self, action: #selector(editingChanged(_:)), for: .editingChanged)
cell.textfield.addTarget(self, action: #selector(didEndEditing(_:)), for: .editingDidEnd)
  @objc
  private func editingChanged(_ sender: UITextField) {

    let center = tableView.convert(sender.center, from: sender.superview)
    guard let idx = tableView.indexPathForRow(at: center) else { return }

    let row = sections[idx.section].rows[idx.row]
    guard case .editable = row else { return }

    let realm = Realm.ex.safeInstance()
    try? realm.write {
      task?.name = sender.text
    }
  }

  @objc
  private func didEndEditing(_ sender: UITextField) {

    let center = tableView.convert(sender.center, from: sender.superview)
    guard let idx = tableView.indexPathForRow(at: center) else { return }

    let row = sections[idx.section].rows[idx.row]
    guard case .editable = row else { return }

    title = sender.text
  }

Build & run demo the update

Add date now

tableView.register(cellType: TextCell.self)
tableView.register(cellType: DatePickerCell.self)
case text(title: String?, details: String?)
case `switch`(text: String, isOn: Bool)
case date(title: String, date: Date)
case datePicker(date: Date)
  //date
  var dateSection = Section(title: L10n.taskDateTitle,
                            rows:
      [
        .switch(text: L10n.taskDateAsk, isOn: task.dueDate != nil)
      ])

  if let dueDate = task.dueDate {
    dateSection.rows.append(Row.date(title: L10n.taskDateAlarm, date: dueDate))
  }

  sections.append(dateSection)
    case .text(let title, let details):
      return textCell(in: tableView, at: indexPath, text: title, details: details)

    case .switch(let text, let isOn):
      let `switch` = UISwitch(frame: .zero)
      `switch`.isOn = isOn
      `switch`.addTarget(self, action: #selector(toggleSwitch(_:)), for: .valueChanged)

      return textCell(in: tableView, at: indexPath, text: text, details: nil, accessoryView: `switch`)

    case .date(let title, let date):
      let details = date.string(dateStyle: .short, timeStyle: .short, in: nil)

      return textCell(in: tableView, at: indexPath, text: title, details: details)

    case .datePicker(let date):
      let cell: DatePickerCell = tableView.dequeueReusableCell(for: indexPath)
      cell.datePicker.date = date
      cell.datePicker.addTarget(self, action: #selector(datePickerChanged(_:)), for: .valueChanged)
      return cell
  @objc
  private func toggleSwitch(_ sender: UISwitch) {

    let center = tableView.convert(sender.center, from: sender.superview)
    guard let idx = tableView.indexPathForRow(at: center) else { return }

    var section = sections[idx.section]

    if sender.isOn {
      section.rows.append(Row.date(title: L10n.taskDateAlarm, date: task?.dueDate ?? Date() ))
    } else {
      section.rows.removeLast(section.rows.count - 1)
    }

    if let row = section.rows.first, case .switch(let text, _) = row {
      section.rows[0] = .switch(text: text, isOn: sender.isOn)
    }
    sections[idx.section] = section

    tableView.reloadSections(IndexSet(integer: idx.section), with: .automatic)
  }
    let row = sections[indexPath.section].rows[indexPath.row]

    switch row {
    case .date:
      toggleDatePicker(onSection: indexPath.section)
    default:
      break
    }
  private func toggleDatePicker(onSection sectionIdx: Int) {

    guard sectionIdx < sections.count else { return }

    tableView.beginUpdates()

    var section = sections[sectionIdx]
    if let row = section.rows.last, case .datePicker = row {
      tableView.deleteRows(at: [IndexPath(row: section.rows.count - 1, section: sectionIdx)],
                           with: .automatic)
      section.rows.removeLast()
    } else {
      section.rows.append(Row.datePicker(date: task?.dueDate ?? Date()))
      tableView.insertRows(at: [IndexPath(row: section.rows.count - 1, section: sectionIdx)],
                           with: .automatic)
    }

    sections[sectionIdx] = section
    tableView.endUpdates()
  }
  @objc
  private func datePickerChanged(_ sender: UIDatePicker) {

    let center = tableView.convert(sender.center, from: sender.superview)
    guard let idx = tableView.indexPathForRow(at: center) else { return }

    var section = sections[idx.section]
    section.rows[1] = Row.date(title: L10n.taskDateAlarm, date: sender.date)
    sections[idx.section] = section
    tableView.reloadRows(at: [IndexPath(row: 1, section: idx.section)], with: .none)

    let realm = Realm.ex.safeInstance()
    try? realm.write {
      task?.dueDate = sender.date
    }

  }
    if let dueDate = task.dueDate, !task.completed {
      text.append(NSAttributedString(
        string: "\n\(dueDate.colloquialSinceNow() ?? "")",
        attributes: [.foregroundColor: UIColor.gray,
                     .font: currentFont.withSize(currentFont.pointSize - 2)])
      )
    }

Build & run demo the update

tableView.register(cellType: NotesCell.self)
case notes(text: String?)
case delete
  //notes
  sections.append(Section(title: L10n.taskNotesTitle, rows: [.notes(text: task.notes)] ))

  //delete
  sections.append(Section(title: nil, rows: [.delete] ))
    case .notes(let text):
      let cell: NotesCell = tableView.dequeueReusableCell(for: indexPath)
      cell.textview.text = text
      cell.textview.delegate = self
      return cell

    case .delete:
      let cell = textCell(in: tableView, at: indexPath, text: L10n.taskDelete, details: nil)
      cell.textLabel?.textColor = UIColor.red
      return cell
case .delete:
  try? task?.delete()
  navigationController?.navigationController?.popViewController(animated: true)
extension TaskDetailViewController: UITextViewDelegate {

  func textViewDidEndEditing(_ textView: UITextView) {

    let realm = Realm.ex.safeInstance()
    try? realm.write {
      task?.notes = textView.text
    }
  }
}
if let notes = task.notes, !notes.isEmpty {

  text.append(NSAttributedString(
        string: "\n\(notes)",
        attributes: [.foregroundColor: UIColor.lightGray,
                     .font: currentFont.withSize(currentFont.pointSize - 3)])
      )
  }

Then demo it and demo the label number of line on tap