UIKit 2 / 1.1. С чего начнём?



Адский крик следит за тобой!

Кор'крон

В этом уроке


Что сделаем?

Мы создаем приложение для списка дел, в которое Вы сможете добавлять свои дела, а также отмечать время их выполения.

Базовые настройки

  1. Создайте Single View Application.
  2. Создайте базовые группы Model, View, Controller
  3. Переместите ViewController в соответствующую папку
  4. Сделайте рефакторинг, переименовав его в MainViewController
  5. Добавьте на полотно контроллера TableView
  6. Задайте ему нулевые отступы
  7. Cоедините его источник данных и делегат с данным контроллером
  8. Создайте для него аутлет tableView
  9. В инспекторе атрибутов добавьте одну ячейку прототип с идентификатором ItemCell
  10. Установите стиль Grouped, это позволит разделить нашу таблицу на секции - в одной будут незавершённые задания, в другой выполненные.
  11. Для ячейки-прототипа установите стиль Subtitile.
  12. Добавьте массив с заголовками секций в контроллер:
    let headerTitles = ["Active", "Completed"]

Вы можете разделить экран не по вертикали, а по горизонтали. Для этого кликните по значку редактора помощника и выберите Assistant Editor on Bottom:


Наша модель списка дел

Создайте в папке с моделями файл Item, а в нём структуру Item:

struct Item {
    let name: String
    let comments: String

    let dueDate: Date
    var completionDate: Date?
}
  1. Имя или краткое название дела в списке дел
  2. Комментарии к заданию
  3. Время, до которого необходимо выполнить дело
  4. Дата выполнения задания. Объевлена опциональной, так как будет присвоена, лишь когда задание будет завершено

Добавьте вычисляемое свойство 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

Мы поступили откровенно плохо, так как мы удаляем первый попавшийся объект совпавший с данным. Мы никак не гарантируем им уникальность. Кроме того, мы не сможем собрать данный код, так как объекты нашего списка не могут быть проверены на равенство, а так как они не являются ссылочными, то найти индекс объекта не выйдет.

Исправим это:

  1. Добавьте UInt-свойство id нашей модели
  2. Добавьте приватное статическое поле для типа currentID, которое по-умолчанию будет равно 0
  3. Сделайте так, чтобы в конструкторе мы присваивали id объекта значение этого поля и увеличивали его на единицу.

Тип будет выглядеть следующим образом:

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
    }
}

Нам достаточно проверить совпадают ли идентификаторы объектов.

Такой подход часто используется для объектов из баз данных - если гарантируется уникальность идентификатора, то нет смысла проверять все поля.

Запустите приложение и сдвиньте ячейку таблицы влево:

Если Вы нажмёте по этой большой кнопке, то произойдёт удаление ячейки. Того же эффекта можно добиться, если продолжить тянуть ячейку влево пока она не уберётся.


NavigationController

В следующем уроке хотим добавить возможность пользователям создавать новые объекты для своего списк дел.

Но начнём мы с контроллера навигации - он позволяет создать стэк из контроллеров, чтобы мы могли перемещаться по ним. Чтобы встроить наш контроллер в контроллер навигации, перейдите по пути Editor -> Embed In -> NavigationController:

Выберите в Document Outline Navigation Controller Scene, а в ней Navigation Bar - это та часть, что будет отображаться наверху приложения.

В инспекторе атрибутов задайте следующие эффекты:

Для красоты перейдите в настройки проекта и выберите в Deployment Info Status Bar Style, равным Light

Выберите область навигации на нагем контроллере и в инспекторе атрибутов введите Title ToDoList.

Запустите приложение и Вы увидите следующее:
А если промотать:

Мы добавили несколько новых пунктов, чтобы показать эффект больших заголовков. Вы можете сделать это сами или пропустить этот шаг - мы всё равно скоро научимся создавать их через само приложение.