Адский крик следит за тобой!
Мы создаем приложение для списка дел, в которое Вы сможете добавлять свои дела, а также отмечать время их выполения.
Model
, View
, Controller
ViewController
в соответствующую папкуMainViewController
TableView
tableView
ItemCell
Grouped
, это позволит разделить нашу таблицу на секции - в одной будут незавершённые задания, в другой выполненные.Subtitile
.let headerTitles = ["Active", "Completed"]
Вы можете разделить экран не по вертикали, а по горизонтали. Для этого кликните по значку редактора помощника и выберите
Assistant Editor on Bottom
:
Создайте в папке с моделями файл Item
, а в нём структуру Item
:
struct Item {
let name: String
let comments: String
let dueDate: Date
var completionDate: Date?
}
Добавьте вычисляемое свойство isCompleted
, которое на основе других свойств будет проверять, выполнена ли работа:
var isCompleted: Bool {
return completionDate != nil
}
Создайте конструктор, который будет принимать первые три поля и будет иметь пустую строку в качестве аргумента по-умолчанию для поля комментариев:
Ответinit(name: String, comments: String = "", dueDate: Date) {
self.name = name
self.comments = comments
self.dueDate = dueDate
}
Вернёмся в наш класс контроллера и добавим свойство items
, которое будет хранить наш список дел:
var items = [Item]()
Добавим два вычисляемых свойства, чтобы отыскивать активные и завершённые дела:
var completedItems: [Item] {
return items.filter{$0.isCompleted}
}
var activeItems: [Item] {
return items.filter{!$0.isCompleted}
}
В viewDidLoad
спопулируйте массив объектов на Ваше усмотрение, сделав так, чтобы хотя бы одна вещь была невыполненной, а другая выполненной:
items.append(contentsOf: [
Item(name: "Make perfect course", comments: "Oh, yeah", dueDate: Date() + Measurement(value: 24, unit: UnitDuration.hours).value),
Item(name: "Make a wish", comments: "Oh, yeah", dueDate: Date() + Measurement(value: 23, unit: UnitDuration.hours).value),
Item(name: "Make a wish", comments: "Oh, yeah", dueDate: Date() + Measurement(value: 23, unit: UnitDuration.hours).value),
])
items[2].completionDate = Date()
Допишем маленькую функцию, которая позволит нам получить объект из массива по его индекс-пути:
func getItem(forIndexPath indexPath: IndexPath) -> Item {
return (indexPath.section == 0) ? activeItems[indexPath.row] : completedItems[indexPath.row]
}
Если получена нулевая секция (невыполненные дела), то стоит извлечь объект из массива активных дел, и наоборот.
В отличие от первых уроков по табличным представлениям вынесем реализацию протоколов в расширения. Начнём с реализации источников данных:
extension MainViewController: UITableViewDataSource {
}
Сделаем так, чтобы возвращалось две секции, а не одна (режим по-умолчанию):
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
Попробуйте сами сделать так, чтобы для каждой секции возвращалось нужное число строк:
Ответfunc tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section {
case 0:
return activeItems.count
case 1:
return completedItems.count
default:
return 0
}
}
Теперь воспользуемся новым методом источника данных:
tableView(_:titleForHeaderInSection:)
Он позволит нам вернуть верный заголовок для каждой секции:
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return headerTitles[section]
}
Попробуйте сами, используя метод получения дела по индекс-пути создать метод для получения ячейки
Ответfunc tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ItemCell", for: indexPath)
let item = getItem(forIndexPath: indexPath)
cell.textLabel?.text = item.name
cell.detailTextLabel?.text = "\(item.dueDate)"
return cell
}
Новинка в данном подходе - dequeueReusableCell(withIdentifier: for:)
, который возвращает неопциональную ячейку.
Запустите приложение и Вы увидите примерно следующее:
Удалять ячейки крайне просто, для этого у делегата есть метод:
tableView(_:commit:forRowAt:)
Он позволяет среагировать на действие, выполняемое после изменения пользователем ячейки таблицы. Добавление этого метода позволяет пользователю удалять ячейки таблицы.
extension MainViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
//Если стиль редактирования - удаление
if editingStyle == .delete {
//Получаем объект по пути
let item = getItem(forIndexPath: indexPath)
//Ищем объект в массиве и удаляем
items.remove(at: items.index(where: {
$0 == item
})!)
//Перезагружаем табличное представление
tableView.reloadData()
}
}
}
Какое табличное представление будет перегружено - переданное в делегат или аутлет? Делегат. При совпадении имён аргументов и свойств объекта будет выбрано первое. Доступ ко второму всё так же можно получить с помощью
self
Мы поступили откровенно плохо, так как мы удаляем первый попавшийся объект совпавший с данным. Мы никак не гарантируем им уникальность. Кроме того, мы не сможем собрать данный код, так как объекты нашего списка не могут быть проверены на равенство, а так как они не являются ссылочными, то найти индекс объекта не выйдет.
Исправим это:
id
нашей моделиcurrentID
, которое по-умолчанию будет равно 0Тип будет выглядеть следующим образом:
struct Item {
let id: UInt
let name: String
let comments: String
let dueDate: Date
var completionDate: Date?
var isCompleted: Bool {
return completionDate != nil
}
private static var currentID: UInt = 0
init(name: String, comments: String = "", dueDate: Date) {
self.name = name
self.comments = comments
self.dueDate = dueDate
self.id = Item.currentID
Item.currentID += 1
}
}
Для кода, имеющего экспозицию в рантайм Objective-C применение такого названия поля
id
приведёт проблемам, так как там это название типа, являющего аналогом Any.
Теперь сделаем наш тип удовлетворяющим протоколу Equtable
:
extension Item: Equatable {
static func ==(lhs: Item, rhs: Item) -> Bool {
return lhs.id == rhs.id
}
}
Нам достаточно проверить совпадают ли идентификаторы объектов.
Такой подход часто используется для объектов из баз данных - если гарантируется уникальность идентификатора, то нет смысла проверять все поля.
Запустите приложение и сдвиньте ячейку таблицы влево:
Если Вы нажмёте по этой большой кнопке, то произойдёт удаление ячейки. Того же эффекта можно добиться, если продолжить тянуть ячейку влево пока она не уберётся.
В следующем уроке хотим добавить возможность пользователям создавать новые объекты для своего списк дел.
Но начнём мы с контроллера навигации - он позволяет создать стэк из контроллеров, чтобы мы могли перемещаться по ним. Чтобы встроить наш контроллер в контроллер навигации, перейдите по пути Editor -> Embed In -> NavigationController
:
Выберите в Document Outline
Navigation Controller Scene
, а в ней Navigation Bar
- это та часть, что будет отображаться наверху приложения.
В инспекторе атрибутов задайте следующие эффекты:
Prefers Large Titles
- режим отображения больших заголовков, которые при скролле уменьшаются и меняют позицию. Введён в iOS 11Bar Tint
- цвет бара навигации. Мы выбрали rgb(241, 84, 59)Style
- Black
, это сделает текст автоматически белымДля красоты перейдите в настройки проекта и выберите в Deployment Info
Status Bar Style, равным Light
Выберите область навигации на нагем контроллере и в инспекторе атрибутов введите Title
ToDoList
.
Мы добавили несколько новых пунктов, чтобы показать эффект больших заголовков. Вы можете сделать это сами или пропустить этот шаг - мы всё равно скоро научимся создавать их через само приложение.