Невероятная «легкость бытия» с Core Data в iOS 10 и Swift 3.

Мы рассмотрим выполнение Задания 5 для курса «Developing iOS 9 Apps with Swift» на Swift 3 для iOS 10.
Почему это интересно?
Особенный интерес связан не столько со Swift 3, сколько с Core Data, который до выхода iOS 10 являлся одним из самых непонятных и сложных в использовании фреймворков, особенно в многопоточной среде.  На WWDC 2016 Apple представила в iOS 10 одну из самых значительных модернизаций Core Data со времен iOS 5.

Возможности новой Core Data в iOS 10

Во главе нового Core Data стоит класс NSPersistenceContainer, представляющий собой прекрасный полноценный Core Data Stack, который очень просто создать и использовать.  Класс NSPersistenceContainer снабжает вас очень простым  API:

screen-shot-2016-10-21-at-5-54-14-pm

Как это видно из названия, управление «видимыми» объектами Core Data осуществляется View Controllers на main queue с использованием viewContext. Очень легко работать с объектами Core Data в фоновом (background) режиме c помощью контекста, возвращаемого методом newbackgroundContext(). Но еще лучше, проще и оптимальнее выполнять различные операции с Core Data в фоновом (background) режиме с помощью метода performBackgroundTask(_:), который сам предоставляет разработчикам NSManagedObjectContext для выполнения этих операций:

screen-shot-2016-10-23-at-7-58-25-pm

Такой простой public API существенно снизит порог вхождения в Core Data.


Все контексты являются независимыми, тут нет взаимоотношений подчинения parent/child. Для получения изменений из другого контекста нужно просто установить флаг automaticallyMergesChangesFromParent в true для вашего контекста. Несмотря на то, что в имени флага присутствует слово *FromParent*, он правильно работает с равноправными контекстами:

screen-shot-2016-10-21-at-7-02-11-pm

В iOS 10 Core Data позволяет выполнять одновременно множество «чтений» и одну «запись» данных без блокировки Хранилища (SQLlite) и контекстов. В предыдущих версиях iOS, когда какой-то контекст читался из Хранилища, все другие контексты должны были ждать, чтобы выполнить свои «чтения» или «записи». Это происходило из-за того, что по логике построения Core Data контакт с Хранилищем проходил через экземпляр класса NSPersistentStoreCoordinator, который был своеобразным «горлышком бутылки», через которое в определенный момент времени мог пройти только один контекст, а это означало, что несмотря на все наши усилия мы не могли полностью искоренить замедление UI на main queue в то время, когда происходит импорт данных и запись данных в базу в фоновом (background) режиме.

Теперь в iOS 10 множество контекстов может читать данные из Хранилища одновременно, так как Apple переместила задачи блокировки контекстов с  NSPersistentStoreCoordinator непосредственно на базу данных SQLite, которая может иметь множество «читающих» каналов и один «пишущий». Это привело к более отзывчивому UI, когда viewСontext способен выбирать данные в то время, как backgroundContext пишет данные на диск. Мы это увидим в демонстрационном примере.

В iOS 10 разработчик получает еще больший контроль над контекстом с помощью супер возможности Query Generation, который позволяет нам сделать мгновенный «снимок» с нашей базы данных и «пришпилить» ее к нашему контексту с помощью одной очень простой строки кода:

 try! container.viewContext.setQueryGenerationFrom(.current)

С этого момента никакие изменения в других контекстах не будут проникать в наш  viewСontext. Для того, чтобы обновить ваш viewСontext после того, как вы сделали необходимые изменения, вам нужно опять вызвать setQueryGenerationFrom(.current). Контекст также автоматически обновляется до самой последней версии, когда вы его сохраняете, заново устанавливаете или когда вставляются изменения из другого контекста. Для использования Query Generations в  Core Data вы должны использовать Хранилище в виде SQLite в WAL режиме (по умолчанию).

Такая организация Core Data в iOS 10 позволяет избавиться от двух существенных недостатков, которые присутствовали в предыдущих версиях:

  • NSPersistentStoreCoordinator управлял доступом к Хранилищу, и контексты были вынуждены ждать друг друга, в результате main queue (то есть UI нашего приложения) блокировался.
  • Часто встречалось аварийное завершение приложение из-за ошибки «CoreData could not fulfill a fault for ..." (Core Data не может восстановить объект ...). Это происходит из-за того, что объект мог быть удален в другом контексте, и доступ к нему осуществлен раньше, чем пришло уведомление о его изменении.

Если у вас есть ограничения на уникальность по какому-то Атрибуту, как, например, в этом случае ограничение на Атрибут unique для фотографии:

screen-shot-2016-11-01-at-10-50-15-am

то вы можете инструктировать Core Data о модификации уже существующих в базе данных объектов с помощью свойства mergePolicy контекста:

screen-shot-2016-11-01-at-11-23-27-am

В этом случае объект с тем же самым уникальным ограничением («unique» Атрибут), находящийся в памяти, «trumps» (перезапишет) версию данных в хранилище.

Еще одна возможность Core Data, которая всячески продвигалась на WWDC 2016 связана с новой функциональностью Xcode 8 генерации subclasses для Сущностей Модели данных Core Data и автоматического управления ими при изменении Модели данных, поддерживая таким образом эти классы в синхронном состоянии с изменяющейся Моделью данных Core Data. Но Xcode запоминает генерируемые файлы для subclasses в директории Derived Data, так что они не участвуют в процессе управления версями в Github, они, по существу, являются «невидимками». Я не уверена, что эта возможность мне пригодится — лично я предпочитаю иметь весь код в репозитории вне зависимости от того, ваш это код или код, сгенерированный системой. Ручной режим генерации subclasses для Сущностей Модели данных Core Data остался, и я предпочитаю пользоваться им для расширения функциональности моих subclasses для Сущностей.

Для cоздания экземпляра Сущности, которая представлена subclass NSManagedObject, теперь используется синтаксис создания экземпляра обычного класса, только нужно указать контекст context: NSManagedObjectContext, в котором он создается. Например:

screen-shot-2016-10-27-at-1-04-11-pm

И не нужно использовать очень многословные конструкции типа:

screen-shot-2016-10-27-at-12-55-59-pm

Запрос и его выполнение также используют более простой синтаксис:

let request: NSFetchRequest = TweetM.fetchRequest()

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

let results = try? context.fetch (request)

Сделано все, чтобы уйти в коде от использования строкового названия Сущности «TweetM« взамен названия соответствующего класса TweetM, который контролируется компилятором.

Демонстрационный пример

Давайте попробуем выполнить Домашнее Задание 5 курса  «Developing iOS 9 Apps with Swift» для iOS 10 и посмотрим, как работают новые изменения в Core Data. Это задание на английском языке доступно на  iTunes в пункте “Programming: Project 5: Smashtag Mentions Popularity″На русском языке вы можете скачать здесь:

Задание 5 iOS 9.pdf


Решение Задания 5 для iOS 10 представлено на Github в папке Assignment 5.
В Задании 5 мы должны сделать небольшое усовершенствование приложения Smashtag (клиент Twitter) в плане проведения некоторого анализа всех меншенов, полученных в результате выборки твитов в Twitter по заданной поисковой строке. Для такого анализа необходимо организовать хранение данных в Core Data, которое предполагает создание Модели Данных, тесно связанной с нашей задачей анализа и предполагает детальное знание предметной области. Поэтому сначала я хочу рассказать в общих чертах о приложении Smashtag, на основании которого мы будем выполнять наше Задание 5.

Приложение Smashtag позволяет получить выборку твитов, удовлетворяющих поисковой строке, которую пользователь может задать произвольным образом. Каждый выбранный твит Tweet содержит text, пользователя user, то есть кто твитит, дату создания created, уникальный идентификатор id, изображения media, которые подсоединены к твиту, хэштэги hashtags, URLs urls и упомянутые в твите пользователи userMentions:

screen-shot-2016-10-22-at-9-00-41-am

Пользователь задает поисковую строку в виде тэга, в нашем случае #autumn, и получает список твитов, которые содержат этот тэг. Вы можете посмотреть каждый отдельный твит на другом экранном фрагменте, который отображает в виде таблицы с различными секциями все его меншены Mentionshashtags urls, userMentions и images:

screen-shot-2016-10-22-at-9-13-16-am

Приложение Smashtag позволяет также посмотреть все изображения в выбранных твитах, а также запомнить строку поиска в NSUserDefauls, чтобы при повторном запуске приложения иметь возможность повторить выборку твитов по старым поисковым строкам.

screen-shot-2016-10-22-at-9-20-06-am

Задание 5 состоит в том, чтобы наделить маленькую кнопочку с буквой i в кружочке на правом экране следующей функциональностью. Если я нажимаю на эту кнопку, то должны быть показаны все меншены с пользователями (users) и хэштегами (hashtags) во всех твитах, когда-либо выбранных с использованием поискового строки, для которой мы нажимали кнопку в буквой i в кружочке. Предполагается, что меншены должны быть уникальны и нечувствительны к регистру. Я сразу покажу готовый результат, чтобы вы понимали поставленную перед нами задачу в Задании 5.

screen-shot-2016-10-22-at-3-42-13-pm

При нажатии кнопки с буквой i в кружочке мы создаем Таблицу Популярности меншенов во всех твитах, возвращенных при поиске по данной поисковой строке (в нашем случае #autumn). Эта таблица должна подчиняться определенным правилам:

  1. она должна быть отсортирована в порядке популярности от наиболее популярных меншенов в верху таблицы к менее популярным внизу,
  2. если два (или больше) меншенов имеют ту же самую популярность, эти меншены появляются в таблице в алфавитном порядке,
  3. каждая строка в таблице показывает не только меншен, но также и сколько раз этот меншен упоминался в твитах, найденных при задании этой поисковой строки,
  4. в таблицу не помещаются меншены, которые упоминаются лишь один раз. Хотя, если вновь выбранный твит включает такой меншен, последний должен немедленно появиться в таблице (потому что тогда он оказывается упомянутым уже больше, чем однократно),
  5. однажды сосчитанные в определенном твите меншены не должны вновь участвовать в подсчете количества упоминаний для того же самого твита, если при выборке этот твит появляется вновь. Другими словами, не должно  быть дублирования меншенов в твитах, которые выбираются более одного раза.
  6. разделите вашу Таблицу Популярности меншенов на две Секции: хэштеги (Hashtags) и пользователи (Users). 

screen-shot-2016-10-22-at-4-40-57-pm

Индекс «H» соответствует хэштегам Hashtags, а индекс «U» — пользователям Users.
От нас не требуется разрушать функционирование остальной части приложения Smashtag. Нет требований в Задании 5 преобразования уже существующих MVC из предыдущего Задания, в MVC,  использующие Core Data, это касается только нового MVC — Таблицы Популярности меншенов. Конечно, нам придется модифицировать уже существующий код для запоминания в Core Data данных, которые мы будем использовать в Таблице Популярности.

Итак, нам нужно загрузить твиты, которые мы получаем с сервера Twitter в результате поиска для заданной поисковой строки, в Core Data и показать специальным образом сформированные для анализа данные из Core Data в отдельном MVC. С точки зрения Core Data это совсем несложная задача, ибо загружаем мы в базу данные в одном MVC, в том, которые показывает нам полученные в результате поиска твиты на первом экране приложения, а показываем — в другом MVC, то есть операции «записи» и «чтения» базы данных разнесены как во времени, так и в пространстве. Для выполнения этих операций нам необходимо сконструировать Модель Данных, то есть определить объекты NSManagedObjects, с которыми мы будем оперировать в базе данных Core Data, и контекст базы данных NSManagedObjectContext.

Модель Данных.

Создание Модели Данных в iOS 10 практически не отличается от предыдущих версий. Будем отталкиваться от того, как должна выглядеть таблица меншенов в этой задаче.

screen-shot-2016-10-22-at-5-12-32-pm

У нас в схеме базы данных будет три Сущности: твит TweetM, поисковая строка (терм) SearchTerm и меншен Mension.

Screen Shot 2016-08-11 at 2.50.12 PM

Атрибуты Сущности Mension диктуются тем, что нам нужно отобразить в таблице популярности меншенов. Сам мешен Mension представлен своими атрибутами keyword и type («Нashtags‘» или «Users«), кроме того, каждый меншен строго «маркируется» только одним поисковым термом term, это четко выделит меншены, относящиеся к определенной поисковой строке term. Только в этом случае мы сможем корректно посчитать требуемое нам количество твитов count, в которых упоминается этот мешен при использовании строки выбора term.
Сущности  Tweet и SearchTerm имеют Взаимосвязь типа «To Many» с обоих сторон, так как понятно, что один и тот же твит может оказаться в выборках для разных поисковых строк. Взаимосвязь terms в Сущности  TweetM, показывает, в каких поисковых термах присутствовал данных твит. Это очень важная Взаимосвязь, с ее помощью мы можем контролировать повторно выбранные твиты для определенной строки поиска SearchTerm  и не обрабатывать их, если они уже присутствуют в базе данных. Таким образом, удается избегать дублирования.
Вы видите, что Сущность Mension  никак не связана с Сущностью TweetM, хотя, конечно, экземпляры этой Сущности «рождаются» именно из твитов. Взаимосвязь между Сущностью Mension и Сущностью TweetM имеет тип «To Many» с обоих сторон, но в этой задаче эта Взаимосвязь нам не нужна.

Конечно, мы создаем subclasses NSManagedObject для наших Сущностей, как это описывалось на  Лекциях 10 и 11.

В iOS 10 это делается немного по-другому. У вас есть возможность автоматического получения subclasses NSManagedObject для наших Сущностей «за сценой». Для этого нужно указать в разделе «Class» параметр «Codegen» равным «Class Definition«. Впрочем, такой режим выставляется по умолчанию:

screen-shot-2016-10-22-at-6-30-31-pm

В этом случае subclasses формируются в служебной директории  Derived Data, которую вы не видите в Xcode, и более того, subclasses всегда поддерживаются в синхронном состоянии с тем, какие атрибуты для этой Сущности вы удаляете или добавляете непосредственно на Модели Данных. Вы можете создавать новые экземпляры Сущности Mension, вы можете изменять их атрибуты и создавать запросы к Mension, совершенно не видя кода  subclass NSManagedObject для Сущности Mension:

screen-shot-2016-10-22-at-6-46-21-pm

Нам нужны пользовательские subclasses для наших Сущностей, которые мы хотим разместить в нашей рабочей директории, поэтому мы изменяем режим  в разделе «Class» параметр «Codegen» c «Class Definition» на «Manuel/ None«:

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

И указываем в параметре «Module» текущую рабочую директорию «Current Product Module«.
Но это еще не все. Дело в том, что subclasses для наших Сущностей могут формироваться либо на языке Objective-C, либо на языке Swift. И даже если у вас весь проект на Swift, почему-то оказывается, что для одной из Сущностей выставлен язык Objective-C.  Очень важно найти, где этот язык указан, а указан он на другой вкладке Инспекторов: не на вкладке Инспектора Data Model, а на вкладке Инспектора Файлов:

screen-shot-2016-10-22-at-8-20-29-pm

Теперь, выбрав в Навигаторе файл с Моделью Данных Model.xcdatamodel, мы можем воспользоваться контекстным меню и добавить subclasses для наших Сущностей:

screen-shot-2016-10-22-at-10-03-19-pm

Сформируются следующие 6 файлов  — на каждую Сущность по 2 файла:

TweetM+CoreDataClass.swift
TweetM+CoreDataProperties.swift
——————————————
Mension+CoreDataClass.swift
Mension+CoreDataProperties.swift
——————————————
SearchTerm+CoreDataClass.swift
SearchTerm+CoreDataProperties.swift

Один из файлов в этой паре, например,  TweetM+CoreDataClass.swift, определяет класс TweetM для Сущности Твит, он, конечно, является subclass NSManagedObject и именно в нем мы будем писать наш пользовательский код:

screen-shot-2016-10-23-at-7-57-57-am

Во втором файле — TweetM+CoreDataProperties.swift — находятся расширения для класса TweetM, в которых в качестве свойств представлены атрибуты Сущности  Твит и даны аксесcоры для доступа к свойству terms, которое является Взаимосвязью типа «To Many» и представлено множеством NSSet.

screen-shot-2016-10-23-at-7-59-59-am

Заметьте, неизвестно по каким причинам генерируется лишнее предложение «import» — его нужно убрать. Так как в Swift есть взаимозаменяемость Set и NSSet, то мы заменим NSSet на «родное» множество Set, и аксессоры нам не понадобятся:

screen-shot-2016-10-23-at-8-23-38-am

Для удобства мы переименуем файлы, содержащие классы:
TweetM+CoreDataClass.swift   в    TweetM.swift
Mension+CoreDataClass.swift  в   Mension.swift
SearchTerm+CoreDataClass.swift  в SearchTerm.swift
В результате мы получили пустые классы  TweetM, Mension и SearchTerm , которые являются subclasses  NSManagedObject и их расширения,  позволяющие обращаться с Атрибутами Сущности как со свойствами:
TweetM+CoreDataProperties.swift

screen-shot-2016-10-23-at-8-23-38-am

Mension+CoreDataProperties.swift
screen-shot-2016-10-23-at-6-24-21-pm

SearchTerm+CoreDataProperties.swift

screen-shot-2016-10-23-at-6-24-51-pm

Получение контекста NSManagedObjectContext

Для работы с базой данных Core Data нужен контекст NSManagedObjectContext, который мы можем получить в AppDelegate стандартным способом — из шаблона, как показано на Лекции 11. В нашем  случае идея та же самая — если у вас уже существующее приложение, то вы должны создать какое-то другое новое приложение и указать, что вам нужен шаблон с Core Data, заимствовать шаблон оттуда, а вновь созданное приложение удалить. В iOS 10 шаблон для Core Data выглядит намного лаконичнее, чем это было в прежних версиях iOS:

screen-shot-2016-10-23-at-8-05-44-pm

Нам предоставляется lazy var persistentContainer, который является экземпляром класса NSPersistentContainer. Напомним, что класс NSPersistentContainer имеет очень простой public API:

screen-shot-2016-10-21-at-5-54-14-pm

и метод saveContext(), который несмотря на весьма общее название сохраняет только контекст на main queueviewContext. Этого метода явно недостаточно, если мы хотим работать с контекстами Core Data как в фоновом (background) режиме, так и на main queue. Поэтому я предлагаю более общее решение, а именно расширение класса NSManagedObjectContext:

AppDelegater.swift

screen-shot-2016-10-23-at-8-32-50-pm

Cуществуют две части приложения, которые мы должны реализовать, чтобы заработал наш новый MVC c Таблицей Популярности меншенов.
Одна часть связана с размещением данных в базе данных, эту часть работы выполняет первый MVC, Tweet Table View Controller, потому что это связано с выборкой данных с сервера Twitter. Когда мы печатаем в текстовом поле с поисковой строкой #stanford, то данные выбираются из сети и доставляются к нам, так что логично загрузить все, что нам было доставлено, в базу данных с TweetM, SearchTerm и Mension именно в этом MVC.
Вторая часть связана с выборкой данных из базы данных и отображении их с помощью второго MVC, Popularity :

screen-shot-2016-10-27-at-5-54-49-pm

Запись данных в Core Data

Давайте выполним первую часть работы. Идем в уже существующий Tweet Table View Controller, обслуживающий его класс  TweetTableViewController, и там будем модифицировать базу данных каждый раз, когда происходит выборка твитов из сети.
Это наш класс TweetTableViewController, надеюсь, вы помните его и эту переменную searchText, представляющую поисковую строку.
Вот код, с помощью которого мы выполняли поиск новых твитов newTweets и вставляли их в таблицу. Здесь же я добавлю метод updateDatabase и передам ему в качестве аргумента новые твиты newTweets и поисковую строку searchText, чтобы он разместил их в базе данных.

TweetTableViewController.swift

screen-shot-2016-10-25-at-11-36-07-am

Здесь нужно быть внимательным. Потому что какой это твит Tweet? Это твит Tweet из фреймворка Twitter или это твит TweetM, который имеет тип NSManagedObject?
В методе  updateDatabase используется Tweet из фреймворка Twitter и мы собираемся разместить его в базе данных. Для этого мне необходим экземпляр NSPersistentContainer, который с помощью одного из своих контекстов NSManagedObjectContext позволит нам размещать объекты в базе данных, искать объекты в базе данных и т.д. Я хочу сделать container частью Модели моего MVC, потому что если думать о том, что делает этот MVC, то он позволяет нам искать твиты, но он также обновляет нашу базу данных. То, какую базу данных обновляет этот MVC, действительно является частью описания Модели. У меня будет переменная var, которую я назову container. Она будет иметь тип NSPersistentContainer и он будет Optional:

screen-shot-2016-10-25-at-11-59-41-am

Если container равен nil, то есть он не установлен, то я не буду обновлять базу данных. Я могу продолжать искать твиты, но база данных не будет обновляться.
Если кто-то хочет обновлять базу данных, то ему лучше установить  container для той базы данных, которую он хочет обновлять. Мы устанавливаем  container значение по умолчанию, которым является persistentContainer в AppDelegate. Теперь у нас есть, где хранить нашу базу данных, и мы можем приступить к реализации нашего метода updateDatabase. Я буду обновлять базу данных в фоновом (background) режиме:

container?.performBackgroundTask {…}

Внутри замыкания выполняется метод класса TweetM:

TweetM.newTweetsWith(twitterInfo: newTweets,andSearchTerm: searchTerm,inContext: context)

который записывает данные в базу данных, и распечатывается статистическая информация об обновленной базе с помощью метода printDatabaseStatistics:

screen-shot-2016-10-25-at-12-43-07-pm

Давайте более подробно рассмотрим метод newtweetsWith:

screen-shot-2016-10-25-at-3-56-14-pm

Сначала из данных выборки из сети, массива [Twitter.Tweet], получаем множество newsSet, которое представляет собой id для выбранных из сети твитов, затем выбираем из Core Data те твиты из этого множества, которые уже записаны в Core Data  для данной строки поиска term. Для существующих в Core Data твитов формируем множество oldsSet, и вычитаем его из первоначального newsSet, получив таким образом действительно новые твиты, которых нет в Core Data и которые следует туда записать.

Получив нужный экземпляр твита tweetM (новый или старый, об этом ниже) из базы данных, мы формируем для него Взаимосвязь «To Many» terms, просто добавляя новый элемент term в множество terms :

tweetM.terms.insert(currentTerm)

Надо сказать, что Core Data достаточно сообразительная, и она автоматически правильно сформирует другую сторону Взаимосвязи tweets со стороны Сущности SeatchTerm, ее дополнительно формировать не надо:

screen-shot-2016-10-25-at-5-26-41-pm

Затем, используя информацию, считанную из сети, twitterInfo[index], формируем меншены:

 Mension.mensionsWith(twitterInfo: twitterInfo[index], andSearchTerm: currentTerm,   inContext: context)

Все экземпляры Сущностей — TweetMSearchTerm и Mension — определяются по одной и той же схеме: вначале запрашивается нужный экземпляр, если он уже есть в Core Data, то он и возвращается в качестве искомого. Если нет, то создается новый экземпляр:

TweetM.swift

screen-shot-2016-10-25-at-5-37-38-pm

SearchTerm.swift

screen-shot-2016-10-25-at-5-49-54-pm

Mension.swift

screen-shot-2016-10-25-at-6-01-53-pm

Обратите внимание, как просто в iOS 10 создается экземпляр Сущности Core Data — как обычный класс, дополнительно нужно указать лишь контекст, в котором этот экземпляр Сущности будет создан:

let tweetM = TweetM(context: context)
let searchTerm = SearchTerm(context: context)
let mentionM = Mension(context: context)

Возвращаемся к методу updateDatabase (), в котором мы сформировали данные для Core Data, записали их в фоновом режиме. Осталось только напечатать статистику полученной базы данных с помощью метода :

screen-shot-2016-10-25-at-6-34-08-pm

Мы распечатаем общее количество твитов, поисковых строк и меншенов, находящихся в данных момент в базе данных,  а также какое количество твитов и меншенов, приходящихся на каждую поисковую строку. Кроме этого, мы распечатываем любопытную информацию о твитах, которые участвуют более, чем в одной поисковой строке, ниже вы увидите, что это как правило, близкие по смыслу поисковые строки — «sea» и «ocean», «sunrise» и «nature»:

screen-shot-2016-10-26-at-11-09-23-am

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

screen-shot-2016-10-26-at-11-13-50-am

Итак, мы получили данные в нашей базе данных. Теперь возвращаемся на storyboard к нашему новому MVC Popularity и мы должны с ним немного поработать.

screen-shot-2016-10-26-at-11-17-14-am

Во-первых, мы должны убедиться, что установили идентификатор повторного использования Identifier для прототипа ячейки. Что это за ячейка? Эта ячейка показывает имя меншена и количество твитов, в которых он упоминается (то есть «популярность»), поэтому я назову ячейку «PopularMentionsCell» и ограничусь стилем ячейки Subtitle.

screen-shot-2016-10-26-at-3-31-15-pm

Мы должны убедиться, что установили пользовательский класс для нового MVC.

screen-shot-2016-10-26-at-3-39-46-pm

Да, он установлен. Здесь все в порядке.

Мы должны убедиться, что установлен идентификатор segue.

screen-shot-2016-10-26-at-3-44-51-pm

Идентификатор segue установлен.

Мы все подготовили.

Нам осталось только реализовать пользовательский класс для нового MVCВсякий раз, когда мы реализуем новый MVC, самая главная вещь, с которой вы должны начать, это Модель данного MVC.  Вы должны понять, что делает этот MVC и какова его Модель.

В нашем случае это простой MVC, он берет некоторую поисковую строку (searchText) типа  #stanford и ищет в нашей базе данных все меншены, связанные с этой поисковой строкой. Понятно, что очень важная часть Модели — searchText, то есть строка типа #stanford.

screen-shot-2016-10-26-at-3-51-12-pm

Но другая действительно важная часть нашей Модели — это база данных. Без нее мы не сможем найти меншены, которые были получены при поиске searchText.

Так что, другая часть Модели — контекст moc типа NSManagedObjectContext. Он будет Optional, но если он равен nil>, то у меня будет пустая Таблица Популярности меншенов. На этот раз это реальное требование.

Если какая-то часть Модели будет изменена, то я выполняю обновление пользовательского интерфейса с помощью метода updateUI(), что в основном означает перезагрузку моей таблицы.

Нам нужно заполнить Таблицу Популярности меншенами, которые получились при поиске строки типа #stanford или какой-то другой.

Как мы будем это делать?

Мы будем использовать NSFetchedResultsController, этот реально крутой объект, который просто “приклеивает” запрос NSFetchRequest к Table View. Для удобства работы с NSFetchedResultsController, профессор Пол Хэгерти предоставил в наше распоряжение прекрасный класс CoreDataTableViewController, который подробно описан в Лекции 11. Я перетяну этот класс в наш проект и сделаю класс PopularityTableViewController наследником CoreDataTableViewController. Нам необходимо установить public API  CoreDataTableController — свойства fetchResultsController. Затем все заработает как “по волшебству”:

screen-shot-2016-10-26-at-4-14-08-pm

Сначала мы создаем запрос request, который затем приклеим к Таблице Популярности. Мы будем выбирать меншены (Сущность Mension),которые привязаны к поисковой строке searchText и у которых число твитов, в которых этот меншен присутствует, больше 1. Далее создаем массив дескрипторов сортировок request.sortDescriptors. Порядок сортировок очень важен, если вы хотите иметь Секции в Таблице Популярности, а именно это нам определено в Задании 5: Таблица Популярности меншенов имеет две Секции: Hashtags и Users, поэтому первым параметром сортировки должен быть тип меншена type, следующим параметром сортировки является count, так как от нас требуется расположить самые популярные меншены в самом верху таблицы, и замыкает список параметров сортировки keyword,  так как при равной популярности требуется расположить меншены в алфавитном порядке. Затем создаем resultsController:NSFetchedResultsController<Mension>? c помощью инициализатора NSFetchedResultsController и делаем его «кастинг» до NSFetchedResultsController<NSFetchRequestResult>. Необходимость последней операции очень подробно описывается в посте «Как заставить работать класс  CoreDataTableViewController в Swift 3».

Мне осталось реализовать метод cellForRowAtIndexPath. Для этого я должен установить идентификатор повторного использования ячейки , запросить объект, соответствующий этой строке у  fetchedResultsController и сконфигурировать ячейку cell:

screen-shot-2016-10-26-at-5-36-41-pm

Каждый раз, когда происходит доступ к базе данных, вы должны размещать его внутри performBlock или performBlockAndWait. Мы должны взять некоторый контекст, в нашем случае мы заимствуем контекст у самого объекта mensionM, и используем  метод performBlockAndWait, внутри которого получаем имя keyword и число упоминаний count этого меншена. Они пригодятся нам для конфигурирования ячейки на main queue. Конфигурирование ячейки связано с доступом к UI, а это может происходить только на main queue. Так получилось, что мы и находимся на main queue, я знаю это, но что, если я пишу код для действительно многопоточного варианта NSManagedObjectContext? Нужно принять это во внимание. Поэтому я не могу обновлять UI в другом, не main queue, потоке.

Вместо этого я создам переменные var keyword и var count типа String, которые я буду использовать как промежуточные переменные при переходе из одного потока в другой:

screen-shot-2016-10-28-at-8-20-54-pm

Нам осталось установить контекст

var moc: NSManagedObjectContext?

который мы получим из предыдущего MVC Resents:

screen-shot-2016-10-26-at-5-53-20-pm

В качестве Модели класс RecentsTableViewController имеет контекст moc для работы с Core Data, но не в фоновом режиме, а на main queuecontainer.viewContext. Именно этот контекст передается MVC Popularity при «переезде» c MVC Resents на MVC Popularity:

screen-shot-2016-10-26-at-6-03-02-pm

Запускаем приложение, выбираем твиты с поисковой строкой #sunrise, получаем не более 100 твитов, эта строка поиска сохраняется, и может быть обнаружена на закладке «History«. Нажимаем на кнопку  с буквой i и получаем Таблицу Популярности меншенов для поисковой строки #sunrise.

screen-shot-2016-10-26-at-6-21-31-pm

Удаление объектов

Хотя этого и требуется в Задании 5, я покажу удаление объектов в Core Data для полноты картины и продемонстрирую метод prepareForDeletion().

В MVC Recents дадим пользователю возможность удалить любую строку с используемой ранее поисковой строкой.

Это делается в методе commit: forRowAt: делегата Data Source :

screen-shot-2016-10-30-at-5-59-26-am

Но объект searchTerm (поисковая строка) имеет Взаимосвязи с Сущностями TweetM и Mension.
В Модели Данных у нас для всех Взаимосвязей  выставлено правило удаление Nullify.

screen-shot-2016-10-30-at-7-00-00-am

Для взаимосвязи tweets мы не можем выставить другое правило удаления, так как один и тот же твит, как мы видели это раньше, может быть задействован в различных поисковых строках близких по смыслу. Поэтому будем удалять mensions и tweets, относящиеся к удаляемой поисковой строке searchTerm, программным образом и будем использовать метод prepareForDeletion() для Сущности SearchTerm:

screen-shot-2016-10-30-at-7-12-15-am

Вначале без всяких условий удаляем все меншены, привязанные к этой поисковой строке (впрочем, это можно было бы сделать и в Модели Данных, задав правило удаления Cascade), но у нас задан режим Nullify и мы выбрали программный способ удаления. Затем удаляем твиты, привязанные к этой поисковой строке, но не все, а только те, у которых больше нет связи с другой поисковой строкой.

И это все, что требуется для удаления объекта Сущности SearchTerm.

Решение Задания 5 для iOS 10 представлено на Github в папке Assignment 5.

Замечание относительно использования UIManagedDocument для получения контекста NSManagedObjectContext

Вариант Задания 5 для Swift 3 и iOS 9 представлен в Github, практически в неизменном виде он будет работать и в iOS 10 Github (папка Assignment 5_UIManagedDocument). Для формирования Запросов мы можем использовать более современный синтаксис типа TweetM.fetchRequest(), а вот создавать Сущности с помощью
let searchTerm = SearchTerm(context: context)
мы не сможем, выдается ошибка «Unacceptable type of value for to-one relationship: property = «term»; desired type = Smashtag.SearchTerm; given type = Smashtag.SearchTerm; value = <Smashtag.SearchTerm: 0x6000000a9f60> (entity: SearchTerm; id: 0x600000036240 <x-coredata:///SearchTerm/t2DE15C7D-9ACA-454B-B3ED-A1F94656C207123> ; data: { mensions =     ( ); term = «#sunrise»; tweets =  ( );})«.

Причина этого пока не известна. Но в iOS 10UIManagedDocument мы можем пока только использовать старый синтаксис:

screen-shot-2016-10-31-at-9-45-38-am

Кроме того, использование API (CoreData + iCloud Drive) упразднено в iOS 10 / macOS 10.12. Для новых приложений Apple рекомендует использовать CloudKit. А это было одно из основных преимуществ использования UIManagedDocument, для получения  NSManagedObjectContext. На фоне очень хорошего API для классического Core Data Stack в iOS 10 привлекательность UIManagedDocument, столь  любимого профессором Полом Хэгерти за его простоту, меркнет.

Один комментарий к “Невероятная «легкость бытия» с Core Data в iOS 10 и Swift 3.

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