IOS-РАЗРАБОТКА
Управление перечислениями. Разработка с помощью Table View
Многие разработчики предпочитают учиться на своих ошибках. Это не всегда правильно, особенно сейчас, когда есть множество возможностей получить знания и навыки из первых уст, например, на авторском курсе по iOS-разработке от Redmadrobot.

Мы перевели для вас полезный туториал по работе с Table View. Смотрите, как легко и просто проапгрейдить стандартное табличное представление и сделать ваше приложение еще удобнее и функциональнее.
2 августа 2018
Keegan Rush
Автор
RW Team Member
Существует ли что-то более фундаментальное в iOS разработке, чем класс UITableView? Его просто и удобно контролировать. Но, к сожалению, «под капотом» спрятано много сложностей: ваш код должен отображать индикатор загрузки в правильное время, обрабатывать ошибки, ожидать завершения сервисных вызовов и отображать результаты, когда они появляются.

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

Чтобы вам было проще разобраться, все изменения мы будем вносить в существующее приложение под названием Chipper. В итоге вы узнаете:

  • Как использовать перечисления (enum) для управления состоянием вашего ViewController.

  • Важность отражения состояния в представлении данных для пользователя.

  • Чем опасно плохо определенное состояние.

  • Как пользоваться обозревателем свойств, чтобы ваши значения оставались актуальными.

  • Как работать с пагинацией, чтобы создать имитацию бесконечной загрузки.
Данный материал предполагает, что вы уже немного знакомы с UITableView перечислениями в Swift. Если у вас возникают вопросы, ознакомьтесь сначала с учебниками по iOS и Swift. Или записывайтесь на курс по введению в iOS-разработку от Redmadrobot.
Начало
Приложение Chipper, которое мы будет переделывать, представляет собой сборник аудиозаписей, со звуками пения птиц из общедоступной библиотеки API xeno-canto.

Если вы ищите, как поет определенный вид птиц, приложение покажет вам список аудиозаписей, которые соответствуют вашему запросу. Вы можете прослушать аудиозапись с пением, нажав на соответствующую кнопку.

Скачайте исходники по ссылке, а затем откройте их в Xcode.
Различные состояния
Хорошо спроектированное табличное представление имеет четыре разных состояния:

  • Загрузка: приложение занято получением новых данных;

  • Ошибка: сбой службы или других операций;

  • Пусто: запрос не вернул никаких данных;

  • Заполнено: приложение отображает полученные данные;

Состояние заполнено — самое очевидное, но другие состояния тоже важны. Вы всегда должны сообщать пользователю о состоянии приложения: показывать индикатор загрузки во время загрузки, сообщать, что запрос ничего не выдал и стоит изменить его параметры, показывать дружелюбное сообщение об ошибке, если что-то пошло не так.

Для начала откройте MainViewController.swift и посмотрите код. ViewController выполняет несколько важных вещей, основываясь на состоянии указанных ему свойств:

  • Отображает состояние загрузки, если значение isLoading равно true.

  • Показывает, что что-то пошло не так, если значение error не равно nil.

  • Если массив recording равен nil или не указан, то на экране отображается сообщение, что пользователю необходимо изменить запрос.

  • Если ни одно из трех вышеуказанных значений не выполнено, то приложение покажет список с результатом запроса.

  • tableView.tableFooterView используется для правильного отображения текущего состояния.

При изменении кода, нужно о многом помнить. А когда вы добавляете новый функционал, ситуация ухудшается, структура кода становится сложной и запутанной.
Плохо определенное состояние
Поищите в MainViewController.swift словое state и вы увидите, что его там нет.
Состояние конечно там есть, но оно не четко определено. Плохо определенное состояние затрудняет понимание того, что делает код, и как он реагирует на изменения его свойств.
Недопустимое состояние
Если значение isLoading равно true, то приложение должно показать состояние загрузки. Если error не равен нулю(nil), то соответственно, приложение должно выдать ошибку. Но что будет, если оба из этих условий будет выполнено? Вы не знаете. Приложение будет в недопустимом состоянии.

MainViewController не может четко определить свое состояние, а значит могут возникнуть ошибки из-за несуществующего или неопределенного состояния.
Улучшенная альтернатива
MainViewController нужен способ получше для управления собственным состоянием. Ему нужен метод, который:

  • Легко понять;

  • Легок в обслуживании;

  • Невосприимчив к ошибкам.

Далее мы сделаем рефакторинг MainViewController и используем перечисления(enum), чтобы усовершенствовать его управление своим состоянием.
Рефакторинг состояний перечисления
В MainViewController.swift, перед объявлением классов, добавьте следующий код:
Это перечисления, которые вы будете использовать для точного определения состояния View контроллера. Далее добавьте свойство для MainViewController, чтобы установить его состояние:
Соберите и запустите приложение, чтобы убедиться, что оно все еще работает. Вы еще не внесли существенных изменений, поэтому все должно быть впорядке.
Рефакторинг состояния загрузки
Первое изменение, которое нужно внести — удалить свойство isLoading в пользу состояния перечислений. В loadRecordings() значение isLoading равен true, а параметр tableVIew.tableFooterView установлен в режиме загрузки. Удалите две строчки в самом начале loadRecordings():
Первое изменение, которое нужно внести — удалить свойство isLoading в пользу состояния перечислений. В loadRecordings() значение isLoading равен true, а параметр tableVIew.tableFooterView установлен в режиме загрузки. Удалите две строчки в самом начале loadRecordings():
Затем удалите self.isLoading = false в блоке fetchRecordings. Финальная версия функции loadRecordings() должна выглядеть так:
Теперь вы можете удалить свойство isLoading для MainViewController`s. Оно вам больше не понадобится.

Соберите и запустите приложение. У вас должно получится следующее:
Свойства для перечислений были установлены, но мы пока с ними еще ничего не делаем. tableVIew.tableFooterView должно отображать текущее состояние. Создадим новую функцию в MainViewController под названием setFooterView():
А теперь вернемся к loadRecordings(). После настройки состояния .loading добавим следующее:
Соберем и запустим приложение:
Теперь, когда вы поменяете состояние на загрузку, setFooterView() вызовет индикатор загрузки на экран. Отличная работа!
Рефакторинг состояния ошибки
loadRecordings() получает записи от NetworkingService. Он принимает ответ от networkingService.fetchRecordings() и вызывает update(response:) , который обновляет состояние приложения.

Внутри update(response:) , если ответ с ошибкой, будет установлено описание ошибки в errorLabel. Для tableFooterView будет установлено значение errorView, которое содержит в себе errorLabel. Найдите в коде следующие строки:
И замените их на:
В setFooterView добавьте новый кейс для состояния ошибки:
View контроллеру больше не нужно его свойство error:Error?, вы можете его удалить. Внутри update(response:) нужно удалить ссылку на свойство error, которое вы удалили ранее:
После удаления этой строки, пересоберите приложение и запустите его.

Вы увидите, что состояние загрузки все еще работает. Но как проверить состояние ошибки? Самый простой способ — отключить интернет на вашем телефоне, если вы используете эмулятор на Mac, разорвите соединение с сетью. Вот что вы должны увидеть, когда приложение попытается получить данные:
Рефакторинг пустого и заполненного состояния
В начале функции update(response:) есть длинная цепочка условий if-else. Чтобы навести порядок, замените все в update(response:) следующим:
Вы только что все сломали, но не беспокойтесь, сейчас все исправим!
Установка правильного состояния
После блока if let error = response.error добавьте следующее:
Не забудьте вызвать setFooterView() и tableView.reloadData() когда будете обновлять состояние. Если вы это забудете, то не увидите изменений.

Теперь найдите строку:
И замените её на:
Вы только что переписали update(response:) который управляет свойствами отображения данных.
Настройка нижнего колонтитула
Здесь вам нужно установить правильное отображение нижнего колонтитула(footer) таблицы для текущего состояния. Добавьте эти два кейса в оператор switch внутри setFooterView()
Приложение больше не использует default кейс, поэтому удалите его.

Соберите и запустите ваше приложение, чтобы увидеть, что произойдет:
Получение данных из состояния
Приложение больше не отображает данные. Свойство View controller`s recordings должно заполнять таблицу, но оно не настроено. Теперь табличное представление должно получить данные из свойства state. Добавим необходимые инструкции:
Это свойство можно использовать для заполнения таблицы. Если состояние равно .populated, то оно заполнит таблицу записями, иначе он вернет пустой массив.

В tableView(_:numberOfRowsInSection:) удалите следующую строку:
И замените её на следующее:
Затем, в tableView(_:cellForRowAt:) удалите блок:
И замените его следующим:
Больше никаких лишних опций!
Вам больше не нужно свойство recordings MainViewController. Удалите его вместе с ссылкой в loadRecordings ().

Соберите и запустите приложение.

Теперь все состояния должны работать. Вы удалили свойства isLoading, error и recordings в пользу одного, четко определенного свойства состояния(state). Отличная работа!
Синхронизация с обозревателем свойств
Удалив плохо определенное состояние из view контроллера, вы можете легко различить все действия поступающие от свойства state. Кроме того, теперь невозможно вызвать состояние ошибки и состояние загрузки одновременно, а это означает, что нет шансов попасть в недопустимое состояние.

Однако есть еще одна проблема. Когда вы меняете значения для свойства state, вы должны помнить, что нужно вызвать setFooterView() и tableView.reloadData(). Если вы этого не сделаете, отображаемые данные не обновятся. Согласитесь, было бы лучше, если бы все обновлялось при каждом изменении состояния.

Это отличная возможность, чтобы воспользоваться didSet property observer (обозреватель свойств). Мы будем использовать его для ответа на изменения значений. Если вы хотите обновлять таблицу и устанавливать нижний колонтитул всякий раз, когда свойство state задано, то необходимо добавить обозревателю свойство didSet.

Замените строки в блоке объявления var state = State.loading на следующие:
Когда значение state получит соответствующий параметр, то сработает вложение didSet. Он вызовет функции setFooterView() и tableView.reloadData() для обновления отображаемых данных.

Удалите остальные вызовы: setFooterView() и tableView.reloadData(), они больше не нужны. Вы можете найти их в loadRecordings() и update(response:).

Соберите и запустите приложение, чтобы убедиться, что оно все еще работает.
Добавление пагинации
Когда вы пользуетесь поиском в приложении, у API есть множество результатов для выдачи, но он не возвращает их все сразу.

Попробуйте поискать в Chipper какую-нибудь популярную птицу, чтобы получить результат с множеством вариантов. Например попугая:
Этого не может быть. Только 50 аудиозаписей с попугаями?

У API xeno-canto есть ограничение — только 500 значений за раз. В нашем приложении установлено ограничение до 50 записей, чтобы было легче с ним работать. Оно находится в NetworkingService.swift

Если мы получаем только первые 500 значений, то как получить все остальные? API, с помощью которого мы работаем, делает это путем пагинации (разбивки на страницы).
Как реализована пагинация в API
Когда вы делаете запрос внутри NetworkingService через API xeno-canto, то он выглядит так:
Результат запроса ограничен первыми 500 элементами. Это и есть первая страница, которая содержит в себе элементы с 1 по 500. Следующие 500 элементов будут второй страницей. Укажите, какую страницу вы хотите получить, в параметре самого запроса:
Обратите внимание на элемент page=2 в конце запроса. Это и есть параметр API, указывающий на то, что вы хотите получить страницу, которая содержит в себе элементы с 501 по 1000.
Поддержка пагинации в Table View
Взгляните на MainViewController.loadRecordings(). Когда он вызывает networkingService.fetchRecordings(), то параметры страницы жестко ограничены одной. Что нужно сделать:

  • Добавьте новое состояние под названием paging.

  • Если ответ от networkingService.fetchRecordings указывает на то, что там больше страниц, чем одна, установите состояние .paging.

  • Когда Table view покажет последний элемент в таблице, загрузите следующую страницу, если состояние равно .paging.

  • Добавьте новые записи в массив из служебного вызова.

Когда пользователь будет прокручивать страницу вниз, приложение будет получать новые результаты. Это создаст ощущение бесконечного списка, как в социальных сетях. Круто, да?
Добавление нового состояния подкачки
Давайте добавим новый кейс paging:
Он будет отслеживать массив отображаемых записей, аналогично заполненному состоянию .populated. Он также будет отслеживать следующую страницу, которую должен получить от API.

Попробуйте собрать и запустить проект. Вы увидите, что он не скомпилируется. Оператор switch в setFooterView переполнен из-за того, что он распроняется на все случаи, так как у него нет default состояния. Добавим следующий код:
Если приложение будет находится в состоянии подкачки, то в конце отображаемой таблицы появится индикатор загрузки.

Однако состояние свойства currentRecordings неполное. Вам нужно его изменить, если вы хотите увидеть нужный результат. Добавим новый кейс внутри currentRecordings:
Настройка состояния для .paging
В update(response:) замените state = .populated(newRecordings) следующим:
response.hasMorePages сообщает количество страниц, которые доступны для текущего запроса через API. Если их несколько, то будет установлено состояние .paging. Если будет только одна страница, то в этом случае состояние будет .populated.

Соберем и запустим приложение:
Если в вашем запросе результат содержит несколько страниц, то в нижней части приложения появится индикатор загрузки. Но если ваш запрос имеет только одну страницу, то вы получите обычное .populated состояние, без индикатора.

Сейчас вы можете увидеть состояние, когда страниц несколько, но кроме отображения индикатора ничего не происходит. Давайте исправим это.
Загрузка страниц
Когда пользователь достигнет конца списка, нужно чтобы приложение начало загрузка следующей страницы. Сперва создадим новый, пустой метод loadPage:
Этот метод мы будем вызывать, когда необходимо будет загрузить следующую страницу результатов NetworkingService.

Помните как в loadRecordings() была реализована загрузка первой страницы? перенесите весь код из loadRecordings() в loadPage(_:), кроме первой строки, где у состояния установлено значение .loading.

Далее измените параметр fetchRecordings(matching: query, page: 1). Должно быть так:
loadRecordings() выглядит немного пустым. Добавим loadPage(_:) с указанием номера страницы, которая должна быть загружена:
Соберем и запустим приложение:
Если ничего не поменялось, то вы на правильном пути!

Добавим после tableView(_: cellForRowAt:) , непосредственно перед return, следующее:
Если текущее состояние будет .paging, а у строки будет такой же индекс, как и у последнего значения массива currentRecordings, то самое время, чтобы загрузить следующую страницу.

Соберем и запустим приложение:
Захватывающе! Когда появляется индикатор загрузки, приложение подгружает следующую страницу с новыми данными. Но оно не добавляет их к текущим записями, а просто заменяет их.
Добавление записей
В update(response:) массив newRecordings используется для отображения нового состояния. Перед инструкцией if response.hasMorePages добавим:
Сначала получаем текущие записи, а затем в массив добавляем новые. Теперь изменим инструкцию response.hasMorePages, чтобы мы могли воспользоваться allRecordings вместо newRecordings:
Видите, как легко было это сделать с помощью перечисления состояний? Соберите и запустите приложение, чтобы увидеть разницу.
На авторском курсе по iOS-разработке от Redmadrobot вы освоите азы iOS-разработки за пару месяцев.

Начните писать чистый код, соответствующий современным гайдам. Учитесь у лучших — ведущие разработчики из Redmadrobot поделятся своими best practices, техниками и инструментами.
Понравилось? Идите учиться большему!
Понравилось? Поделитесь с друзьями