Как заставить работать класс CoreDataTableViewController в Swift 3

screen-shot-2016-10-15-at-9-44-52-pm

Этот пост написан в продолжение поста «Как работать с курсом «Developing iOS 9 Apps with Swift»  в Xcode 8 с Swift 2.3  и Swift 3″.  Дело в том, что в Swift 3  запрос NSFetchRequest<NSFetchRequestResult> стал Generic, а следовательно, стал Generic и класс NSFetchResultsController<NSFetchRequestResult>. В результате возникли некоторые трудности при использовании в Swift 3 фантастически удобного класса CoreDataTableViewController, который разработан профессором Полом Хэгерти и предоставлен студентам Стэнфорда для удобной работы с данными Core Data в таблицах Table View. В этом посте я показываю, как эти трудности разрешить и заставить работать класс CoreDataTableViewController в Swift 3, и не только в iOS9, но и в iOS 10.

Вначале  очень кратко напомню  о том, откуда появился класс  CoreDataTableViewController. Когда у вас огромное количество информации в базе данных, то прекрасным средством показа этой информации является Table View. В 99% случаев либо Table View, либо Collection View используются для показа содержимого больших баз данных. И это настолько распространено, что Apple обеспечила нас в iOS прекрасным классом NSFetchedResultsController, который “подвязывает” запрос NSFetchRequest к таблице UITableView
И не только “подвязывает” лишь однажды, а эта “подвязка” действует постоянно и, если в базе данных каким-то образом происходят изменения, NSFetchRequest возвращает новые результаты и таблица обновляется. Так что база данных может меняться “за сценой”, но таблица UITableView всегда остается в синхронизированном с ней состоянии. Это действительно очень очень круто.
 NSFetchResultsController обеспечивает нас методами протоколов UITableViewDataSource и UITableViewDelegate, такими, как numberOfSectionsInTableView, numberOfRowsInSections и т.д. Единственный метод, который он не реализует, — это cellForRowAtIndexPath. Вам самим придется реализовать его, потому что для реализации метода cellForRowAtIndexPath нужно знать пользовательский UI для ячейки таблицы, а вы — единственный, кто знает, какие данные и как они размещаются на экране. Но что касается других методов протокола UITableViewDataSource, даже таких, как sectionHeaders и всего остального, NSFetchedResultsController берет все на себя.

Как работать с NSFetchResultsController?


От вас потребуется только создать запрос  request, настроить его предикат и сортировку, а выводом данных в таблицу займется  NSFetchResultsController.
NSFetchResultsController также наблюдает за всеми изменениями, происходящими в базе данных, и синхронизирует их с Table View.
Способ, каким она это делает, связан с делегатом NSFetchResultsControllerDelegate, методы которого вам предлагается без изменения скопировать из документации в ваш класс.

«Ну вот, я думал, что настроить NSFetchResultsController — это просто, а тут выясняется, что я должен реализовать методы делегата NSFetchResultsControllerDelegate?» — подумаете вы.
Но вам повезло, всю эту работу профессор сделал за вас и предоставил в ваше распоряжение замечательный класс с именем CoreDataTableViewController.
Он не только скопировал весь необходимый код из документации по NSFetchResultsController, но и переписал его с Objective-C на Swift.  
Теперь, для того, чтобы ваш UITableViewController унаследовал всю функциональность NSFetchResultsController, вам достаточно сделать  CoreDataTableViewController вашим superclass  и определить public var с именем fetchedResultsController. Вы устанавливаете эту переменную, и CoreDataTableViewController будет использовать ее для ответа на все вопросы UITableViewDataSource, а также делегата NSFetchedResultsController, который будут отслеживать изменение базы данных.
В итоге вам всего лишь нужно:

  1. установить переменную var fetchedResultsControlleи
  2. реализовать метод cellForRowAtIndexPath.

Профессор очень подробно показал на Лекции 10 и Лекции 11 на слайдах и в демонстрационном примере Smashtag L11, как нужно использовать класс CoreDataTableViewController.
Я не буду вдаваться в подробности, скажу лишь, что для показа пользователей Twitter, которые были записаны в Core Data при выборке твитов с определенной поисковой строкой, был использован обычный Table View Controller, который обслуживался классом TweetersTableViewController (очень подробно работа этого класса освещается в Лекции 11)
.
В этом классе создается NSFetchResultsController с помощью инициализатора, включающего в качестве аргумента запрос request, а затем присваивается переменной var с именем fetchedResultsController вновь созданный NSFetchResultsController. Как только вы это сделаете, таблица c пользователями начнет автоматически обновляться (Swift 2.3):

screen-shot-2016-10-15-at-6-37-29-pm

Конечно, вам нужно реализовать метод cellForRowAtIndexPath. Но это и действительно все, что нужно сделать (Swift 2.3) :

screen-shot-2016-10-15-at-6-41-26-pm

Итак, реализовать cellForRowAtIndexPath и установить var fetchedResultsController.
Получаем таблицу пользователей, которые создали твиты c заданным меншеном, например,  #nature, или какой-то другой:

screen-shot-2016-10-15-at-5-55-56-pm

Все очень здорово и просто в Swift 2.3, но в Swift 3 запрос NSFetchRequest<NSFetchRequestResult> стал Generic, а следовательно, стал Generic и класс NSFetchResultsController<NSFetchRequestResult>.
Теперь запрос, участвующий в классе TweetersTableViewController выглядит в Swift 3 следующим образом:

screen-shot-2016-10-15-at-6-29-06-pm

И нам придется сформировать промежуточную переменную resultsController, у которой тип будет NSFetchResultsController<TwitterUser>?. Переменная public var с именем fetchedResultsController в CoreDataTableViewController тоже стала Generic в Swift 3:

screen-shot-2016-10-15-at-6-49-49-pm

По идее и класс CoreDataTableViewController нужно сделать Generic, но мы этого делать не будем, потому что его subclasses, например, такие, как приведенный выше TweetersTableViewController, испольуются на storyboard, а на storyboard не работают Generic классы.
Как же нам быть? Класс CoreDataTableViewController чрезвычайно удобный и позволяет избежать дублирования кода во всех Table View, работающих c Core Data?
Поэтому мы поступим следующим образом: оставим в CoreDataTableViewController у переменной public var с именем fetchedResultsController тип NSFetchedResultsController<NSFetchRequestResult>? и будем выполнять «кастинг типа» ВВЕРХ для промежуточной переменной resultsController в приведенном выше коде для Swift 3:

screen-shot-2016-10-15-at-8-06-46-pm

Такой «кастинг типа» ВВЕРХ для ТwitterUser всегда будет правильно работать, так как NSManagedObject, которым и является ТwitterUser, реализует протокол NSFetchRequestResult:

screen-shot-2016-10-15-at-8-23-37-pm

А когда мы заполняем ячейку таблицы реальными данными в методе cellForRowAtInfexPath, мы выполняем обратную операцию — «кастинг типа» ВНИЗ от NSFetchRequestResult до ТwitterUser (Swift 3):

screen-shot-2016-10-15-at-8-30-22-pm

Все это прекрасно работает, так что парой дополнительных строк мы заставили CoreDataTableViewController работать в Swift 3.

P.S. Можно обойтись еще более простым кодом в Swift 3 и исключить промежуточную переменную resultsController, если выполнять «кастинг типа» ВВЕРХ сразу для запроса request:

screen-shot-2016-11-23-at-4-30-18-pm


Кстати именно такой код предлагает при переходе со Swift 2 на Swift 3 миграционный «робот» в Xcode 8.1 и 8.2, который можно запустить с помощью меню Edit->Convert->to Current Swift Syntax. Так что заставлять работать CoreDataTableViewController не придется — его работа будет обеспечена автоматически при использовании миграционного инструмента.

Код демонстрационного примера Smashtag L11 (в папке Lecture 11) для Swift 2.3 находится на Github, а для Swft 3 — на Github.

Все это работает и в iOS 10, но в iOS 10 появились новые возможности работы с Core Data: вместо кода на несколько экранов для Core Data Stack появился компактный persistentContainer, автоматическая и ручная генерация subclasses для NSManagedObjects, Core Data стал более лаконичным. Например, создание нового элемента Сущности Tweet выглядит так:

let tweet = Tweet (context: context)

а создание запроса  и его выполнение так:

 let request: NSFetchRequest<Tweet> = Tweet.fetchRequest()

.  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .

 let results = try? context.fetch (request)

Пост «Невероятная «легкость бытия» с Core Data в iOS 10 и Swift 3.» посвящен работе Core Data в iOS 10 в рамках демонстрационного примера  Smashtag L11 и Домашнего Задания 5, который можно посмотреть на Github в папке Assignment_5.

Как заставить работать класс CoreDataTableViewController в Swift 3: 3 комментария

  1. Вот смотрю на все это и думаю: «Неужели я, начиная изучение программирования и swift в частности, через некоторое время смогу полностью понимать о чем написано в этой статье — кажется вообще не реально»

    • Если прочтете все лекции и выполните все домашние Задания, то — ДА. В этом и цель стэнфордских курсов. Это реально трудно, но вполне достижимо. Поверьте, я думала точно также как вы, когда начинала.

Обсуждение закрыто.