Core Data в iOS 9 и Swift при ограничениях на уникальность.Часть 2.

Screen Shot 2016-04-07 at 5.00.29 PM
Это вторая часть поста. В первой части создается Модель Данных и  экспериментальное приложение CoreData1Swift (Github) с классическим Core Data Stack для получения MOC. В этой части мы рассмотрим другие способы получения MOC, а также различные варианты распространения MOC на другие View Controller пользовательского интерфейса.

Работа с Core Data через UIManagedDocument

В этом разделе для получения MOC вместо Core Data Stack используется UIManagedDocument, который берет на себя все проблемы с созданием стэка Core Data, в том числе и контекста UIManagedObjectsContext, все проблемы с безопасностью многопоточной обработки и т.д. Кроме того, это самый простой способ самостоятельного изучения Core Data и самый короткий способ перехода к  iCloud.

Для экспериментального исследования на основе приложения CoreData1Swift (Github) из предыдущих разделов создадим приложение CoreData2Swift, в котором заменим класс CoreDataStack классом Database для получения экземпляра document класса MyDocument, который наследует от UIManagedDocument. Класс MyDocument нужен нам для того, чтобы имя нашего хранилища включало расширение .sqlite, что позволит нам очень просто следить за изменением базы данных типа SQLite с помощью таких сторонних приложений, как Base.
Database.swift
Screen Shot 2016-04-07 at 9.49.31 AM

Screen Shot 2016-04-07 at 9.50.52 AM

В свою очередь UIManagedDocument наследует от класса UIDocument.
UIDocument — это целый набор механизмов для управления хранилищем. UIManagedDocument размещает базу данных Core Data в некотором таком хранилище.
Но все, что вы реально можете делать с этой базой данных — это создать, открыть UIManagedDocument, получить его контекст NSManagedObjectContext и использовать в управлении вашей Core Data базой данных.
В отличие от классического CoreDataStack, где сохранение Core Data данных эквивалентно сохранению контекста MOC, в случае с UIManagedDocument сохранение базы данных  происходит на уровне документа и только тогда, когда самому документу это кажется уместным, работает AUTOSAVE.
При создании экземпляра UIManagedDocument мы используем “назначенный” инициализатор, которым в нашем случае будет MyDocument (fileURL: ). Полученный таким образом документ еще не существует на диске. Нам нужно еще проделать некоторую работу, чтобы он существовал на диске. Как мы будем это делать?
У нас есть url нашего файла и мы должны узнать, существует ли файл с заданным url  с помощью метода fileExistsAtPath, возвращающего Bool значением.
Если файл существует, то мы собираемся его открыть с помощью метода openWithCompletionHandler: для экземпляра класса UIManagedDocument.
Если файл не существует, то мы собираемся создать его на диске с помощью метода saveToURL:forSaveOperation:completionHandler: и .ForCreating в качестве значения для аргумента forSaveOperation:
Database.swift

Screen Shot 2016-04-07 at 9.54.29 AM

Значение .ForCreating, для аргумента forSaveOperation применяется, если мы создаем новый документ.
Почему обоим методам нужен completionHandler?
Потому что все операции c document: open,save, create,  выполняются асинхронно, а completionHandler будут выполняться в том потоке, в каком вызывались эти методы. Так как UIManagedDocument является UIKit объектом, то все его методы вызываются в main queue.
Мы хотим заставить наш document правильно работать с дупликатами, а для этого нужно присвоить  “политике управления конфликтами при слиянии данных” (merge policies) значение, равное NSMergeByPropertyObjectTrumpMergePolicy. Политика устанавливается для определенного контекста MOC.
Но экземпляр document класса UIManagedDocument имеет два контекста:

  1. один — корневой контекст типа NSPrivateQueueConcurrencyType (для private queue), а
  2. другой, managedObjectContext,  — дочерний контекст типа NSMainQueueConcurrencyType  (для main queue).

Пользовательский интерфейс (UI) работает с document.managedObjectContext на main thread.
Чтение/запись происходят автоматически в фоновом потоке. UIManagedDocument сам позаботится об этом. Вы вообще не чувствуете присутствия корневого контекста, работающего в private queue.

Но у нас особая ситуация — в Модели Данных выставлены ограничения на уникальность атрибута unique для сущности Photo и атрибута name для сущности Photographer, поэтому, как мы уже поняли из предыдущего раздела, для автоматического разрешения конфликтов нам придется устанавливать значение “политики” равное NSMergeByPropertyObjectTrumpMergePolicy для каждого из контекстов внутри их собственных потоков.

То есть вы должны использовать document.managedObjectContext внутри main thread. Если вы хотите использовать его в другом потоке, вы должны использовать либо метод performBlock, либо performBlockAndWait. Подобным же образом, если вы не знаете точно, что вы запущены в private thread для parentСontext, вы должны также использовать performBlock.
Вот как выглядит установка “политики управления конфликтами при слиянии данных”  для обоих контекстов документа document:
Database.swift

Screen Shot 2016-04-07 at 2.44.04 PM

Вам не нужно сохранять UIManagedDocument, так как он сохраняется автоматически.
Но вы можете его сохранить, используя тот же метод, что и при открытии документа с другим значением для параметра forSaveOperation:.ForOverwriting. В нашем классе MyDocument мы можем принудительно сохранять документ с помощью метода saveDocument():
MyDocument.swift

Screen Shot 2016-04-07 at 2.46.28 PM

Это очень важная вещь — AUTOSAVE.
Вторая важная вещь — закрытие документа. Закрытие также происходит автоматически, то есть работает AUTOCLOSE. Когда документ закрывается? Если нет strong указателей на документ, он покидает “кучу” (heap), и происходит автоматическое закрытие.
В общем случае совсем не нужно вызывать метод close, но вы также можете это делать. И закрытие, и сохранение происходят асинхронно, и вы получаете completionHandler, чтобы сделать то, что вы хотите после завершения операции. Перед закрытием документа происходит AUTOSAVE.

Замечание. Относительно AUTOSAVE необходимо отметить, что если вы модифицируете  свою базу данных, а затем останавливаетесь для отладки, делаете какие-то изменения и вновь запускают приложение, то измененных данных в базе данных может не оказаться. Потому что не было возможности для срабатывания AUTOSAVE. Поэтому, если вы находитесь в процессе разработки и применяете отладчик для остановки приложения, то лучше сделать явное сохранение изменений в базе данных.
Для “подкачки” фотографий  c сервера Flickr, загрузки их в Core Data мы имеем в приложении класс JustPostedFlickrPhotosTVC, который является subclass класса PhotosCDTVC:
JustPostedFlickrPhotosTVC.swift

Screen Shot 2016-04-07 at 2.52.49 PM

Для работы с Core Data нам нужен экземпляр database класса Database. Как только мы его установим (об этом немного позже), срабатывает Наблюдатель свойства didSet{}, в котором определяется контекст self.moc и вызывается метод fetchPhotos выборки данных с Flickr  сервера и записи их в Core Data. Метод fetchPhotos имеет точно такой же вид, как и в случае классического CoreDataStack, но запись результатов в Core Data происходит через document:
JustPostedFlickrPhotosTVC.swift

Screen Shot 2016-04-07 at 2.56.18 PM

Запись документа document можно вообще не использовать, тогда через некоторое время (iOS сама выбирает интервал времени) произойдет AUTOSAVE. И в этом временном промежутке можно наблюдать присутствие дубликатов на экране. Но как только произойдет AUTOSAVE, то все дубликаты исчезнут с экрана.
Для того, чтобы отслеживать срабатывание AUTOSAVE, нужно переопределить один метод в классе MyDocument:
Database.swift

Screen Shot 2016-04-07 at 2.59.35 PM

В остальном приложение будет работать точно также, как и в случае с классическим Core Data Stack.

Код экспериментального приложения с UIManagedDocument находится в приложении CoreData2Swift (Github).

Core Data Stack c двумя контекстами MOC, связанными отношением Parent-Child

Core Data супер эффективна в выборке и записи данных, и в большинстве случаев вы можете работать с ней непосредственно на main queue. Но иногда, когда у вас идет работа с очень емкими данными типа изображений, вам приходится обеспечивать работу Core Data в многопоточной среде. Мы уже видели работу Core Data в многопоточной среде на примере UIManagedDocument, но это очень “непрозрачная” работа, ведь мы даже не подозревали, что в private thread работает “пищущий” MOC.
Начиная с  iOS 5 Apple представила разработчикам Parent-Child решение. В этом решении каждый MOC работает в своем собственном потоке. Как только работа сделана, выполнение возвращается в вызывающий поток. В этом решении когерентность между MOCs осуществляется не слиянием (merging) и посылкой уведомлений (notifications), а тем, что MOC s — являются вложенными, благодаря Parent-Child отношению. Когда выполняется операция save в контексте Child MOC, изменения автоматически выталкиваются в контекст Parent MOC, осуществляя согласованность контекстов.
Для этого случая гуру по Core Data Marcus Zara предлагает решение, которое устраивает практически всех, за исключением каких-то экстренных случаев.Во-первых, его Core Data Stack состоит минимум из двух экземпляров NSManagedObjectContext:

1 Private Queue контекст. Этот контекст делает одну работу — пишет на диск. Такая очень простая и в то же время жизненно важная работа в приложении. Мы создаем его в private queue, потому что действительно хотим, чтобы контекст был асинхронным по отношению к  UI. Мы хотим избежать блокировки UI из-за работы с хранилищем.
2 Main Queue контекст.Это “единственный источник истины” для приложения. Приложение будет использовать этот контекст для всех взаимодействий с пользователем. Если нам необходимо что-то показать пользователю,  мы используем этот контекст. Если пользователь собирается что-то редактировать,  мы используем этот контекст. Никаких исключений.

Итак, это минимальные два контекста. Но может быть и больше, потенциально может быть много. Дополнительные контексты должны быть Сhild-контекстами для Main Queue контекста. Их работа может быть самой разнообразной, но всегда НЕ связанной с манипулированием данными Core Data пользователем.
Для экспериментального исследования Parent-Child MOC s, на основе приложения CoreData1Swift (Github) создадим приложение CoreData3Swift(Github), в котором в классе CoreDataStack к контексту mainMoc добавим еще один контекст privateMoc и свяжем их отношением Parent-Child:
CoreDataStack.swift

Screen Shot 2016-04-07 at 3.05.14 PM

У Parent “пищущего” контекста privateMoc определен persistentStoreCoordinator, то есть он будет писать напрямую в SQLite. У Child контекста mainMoc вместо persistentStoreCoordinator определено свойство parentContext. У обоих контекстов указана “политика разрешения конфликтов при слиянии” как NSMergeByPropertyObjectTrumpMergePolicy, что означает полную замену старых значений новыми.
Еще нам необходимо скорректировать метод saveMainContext(), добавив запись в родительский контекст privateMoc
:
CoreDataStack.swift

Screen Shot 2016-04-07 at 3.08.10 PM

В основном код класса PhotosCDTVC не отличается от соответствующего кода для классического CoreDataStack, за исключением того, что для записи изменений в Core Data ( а у нас в этом классе есть удаление объектов) нам недостаточно просто MOC, нам нужен экземпляр стэка coreDataStack, чтобы произвести запись с использованием метода saveMainContext():
PhotosCDTVC.swift

Screen Shot 2016-04-07 at 3.12.46 PM

Приложение CoreData3Swift (Github) работает точно также, как и классический вариант, но имеет лучшие временные характеристики по блокировке UI.
В статье Marcus Zara говорит, что должно быть по крайней мере 2 MOC:  один “пишущий”, другой — для UI,  но возможны и другие private контексты MOC, если они не имеют дело с UI.  Они должны быть дочерними по отношению к mainMOC.
У нас как раз тот самый случай: мы закачиваем данные с сервера Flickr в Core Data, минуя  пользовательский интерфейс. Давайте создадим в новом приложении CoreData4Swift для этой работы контекст workMOC прямо в нашем классе JustPostedFlickrPhotosTVC:

JustPostedFlickrPhotosTVC.swift

Screen Shot 2016-04-07 at 3.15.58 PM

Так как контекст workMOC работает в private queue, то создавать объекты в этом контексте можно только с помощью метода perfomBlock, а сохранять — с помощью метода perfomBlockAndWait:
JustPostedFlickrPhotosTVC.swift

Screen Shot 2016-04-07 at 3.19.32 PM

Как мы видим вся работа по “закачки” данных уходит из main queue и приложение CoreData4Swift  (Github) (как покажут экспериментальные исследования)  обладает очень малым временем блокировки UI.

Так как размещение Core Data данных происходит в private queue, то можно наблюдать дубликаты фотографий на экране, которые очень быстро уходят при записи контекста.

Некоторые исследования времени блокировки UI при различных Core Data Stack

Мы будем экспериментировать на симуляторе, и прежде, чем запустить приложение, мы удалим приложения с симулятора с тем, чтобы проверить работу на “чистой” базе. Считывалось 250 фотографий с сервера  Flickr  и записывалось в Core Data.

Вот результаты начальной загрузки:

  1. Классический CoreDataStack  — 123 /1000 ms>
  2. UIManagedDocument               —  95  /1000 ms
  3. CoreDataStack c Parent-Child   —  94 /1000 ms
  4. CoreDataStack c Parent-Child-Work   —  6 /1000 ms

Если мы запустим приложения практически сразу же, предварительно его не удаляя , то начнут действовать механизмы “разрешения конфликтов при слиянии данных” и результат изменится:

  1. Классический CoreDataStack  — 405 /1000 ms
  2. UIManagedDocument                — 383 /1000 ms
  3. CoreDataStack c Parent-Child   — 376 /1000 ms
  4. CoreDataStack c Parent-Child-Work —  262 /1000 ms

Понятно, что обработка дупликатов занимает значительное время, если их много (практически все только что “закачанные” в базу данные — около 250). Понятно, что нерационально записывать практически то же самое дважды и заставлять Core Data обрабатывать дупликаты.

Попробуем использовать другую “политику” — NSMergeByPropertyStoreTrumpMergePolicy.

Получаем следующие результаты при начальной загрузке:

  1. Классический CoreDataStack  — 182 /1000 ms
  2. UIManagedDocument               —  127 /1000 ms
  3. CoreDataStack c Parent-Child   —  127 (25) /1000 ms
  4. CoreDataStack c Parent-Child-Work   —  6 /1000 ms

при дополнительно “подкачке”:

  1. Классический CoreDataStack   504 /1000 ms
  2. UIManagedDocument                 384 /1000 ms
  3. CoreDataStack c Parent-Child   — 363 /1000 ms
  4. CoreDataStack c Parent-Child-Work   —  273 /1000 ms

Результаты практически те же самые и не зависят от “политики”. Как и ожидалось, варианты 2 и 3 дали практически одинаковые результаты. Наиболее выгодным с точки зрения меньшего времени блокирования  UI оказался 4-ый вариант — СoreDataStack  c Parent-Child-Work. В остальных случаях этот показатель можно улучшить, если использовать специальный алгоритм, учитывающий специфику нашей задачи.
В нашем случае (когда данные меняются во времени незначительно) проще сделать выборку данных из базы, определить новые данные в порции и записать только их. Для этого создадим метод newPhotos класса Photo:
Photo.swift

Screen Shot 2016-04-07 at 3.28.31 PM

На вход этого метода поступают JSON данные json, полученные с сервера Flickr,  и контекст Core Data context. Мы выбираем из базы данных все фотографии и интересуемся только их атрибутом unique. В результате получаем массив строк

var uniques: [String]

Извлекаем из JSON данных аналогичный массив

let uniquesFlickr: [String]

Превращаем эти массивы в множества, и вычитаем из множества новых значений старые. В результате получаем несовпадающие значения атрибута unique. Затем пишем соответствуюшие им фотографии в Core Data.

Использование этого алгоритма дает следующие результаты при начальной загрузке:

  1. Классический CoreDataStack  — 128 /1000 ms
  2. UIManagedDocument               —  90  /1000 ms
  3. CoreDataStack c Parent-Child   —  91 (20) /1000 ms

при дополнительной “подкачке”:

  1. Классический CoreDataStack   0 новых — 0 /1000 ms 25 новых — 32
  2. UIManagedDocument                 0 новых  — 0.1  /1000 ms  9 новых — 11
  3. CoreDataStack c Parent-Child  — 0 новых —  0  /1000 ms 3 новых — 0

Распространение контекста NSManagedObjectContext методом Dependency Injection

На основе приложение CoreData3Swift (Github) создадим приложение CoreData4Swift (Github), в котором будут два экранных фрагмента : Photographers — для фотографов, а Flickr Photos — для фотографий:

Screen Shot 2016-04-07 at 3.44.30 PM

“Закачка” фотографий с сервера Flickr будет производится все в том же классе JustPostedFlickrPhotosTVC, но только теперь он будет наследовать не от таблицы с фотографиями PhotosCDTVC, а от таблицы с фотографами PhotographersCDTVC:
JustPostedFlickrPhotosTVC.swift

Screen Shot 2016-04-07 at 3.46.32 PM

Класс для списков фотографов PhotographersCDTVC построен по тому же принципу, что и класс PhotosCDTVC. Это обобщенный класс который наследует от CoreDataTableViewController и отображает выбранные из Core Data объекты Photographer независимо от того, каков критерий их выборки. В этом классе всего два метода cellForRowIndexPath и prepareForSegue:
PhotographersCDTVC.swift

Screen Shot 2016-04-07 at 3.49.04 PM

В дочернем классе JustPostedFlickrPhotosTVC как только из AppDelegate установлен стэк coreDataStack, берем контекст coreDataStack.mainMoc и конфигурируем свойство fetchedResultsController для родительского класса PhotographersCDTVC — единственное public свойство класса CoreDataTableViewController, с которым мы работаем:
JustPostedFlickrPhotosTVC.swift

Screen Shot 2016-04-07 at 3.51.50 PM

Screen Shot 2016-04-07 at 3.52.59 PM

Для показа в таблице фотографов мы выбираем из >Core Data всех фотографов и сортируем их по имени.
Пользователь указывает в таблице  фотографа и получает таблицу его фотографий, а выбрав фотографию, получает ее изображение:

Screen Shot 2016-04-07 at 3.54.45 PM

В этом разделе нам интересна лишь передача MOC от таблицы с фотографами к таблице с фотографиями.
Для показа фотографий определенного фотографа создадим subclass PhotosByPhotographerCDTVC класса PhotosCDTVC, конечно, центральное место в нем займет свойство, связанное с конкретным фотографом:

Screen Shot 2016-04-07 at 3.57.03 PM

Как только мы устанавливаем фотографа photographer, так сразу же можем определить контекст moc: UIManagedObjectContext? и  сконфигурировать свойство self.fetchedResultsController для выборки из Core Data фотографий при условии, что атрибут whoTook равен установленному фотографу photographer.
Это и есть распространение MOC методом Dependency Injection: мы передаем View Controller объект, содержащий MOC.
Установка фотографа происходит в классе PhotographersCDTVC, являющимся родительским для класса JustPostedFlickrPhotosTVC, который обслуживает экранный фрагмент таблицы с фотографами:

Screen Shot 2016-04-07 at 4.00.25 PM

Приложение CoreData4Swift, сопровождающее этот раздел, находится на  (Github)

Распространение контекста NSManagedObjectContext методом
Singleton

Согласно официальной документации в Swift Singleton для нашего класса CoreDataStack формируется как константа класса с private инициализатором:

Screen Shot 2016-04-07 at 4.43.36 PM

Для доступа к Singleton достаточно указать константу класса CoreDataStack.defaultStack и можно пользоваться этим экземпляром стэка в любом месте программы.
В классе JustPostedFlickrPhotosTVC:

Screen Shot 2016-04-07 at 4.46.06 PM

В классе PhotosByPhotographerCDTVC:

Screen Shot 2016-04-07 at 4.50.58 PM

В AppDelegate:

Screen Shot 2016-04-07 at 4.52.26 PM

Использование CoreDataStack.defaultStack везде одинаковое и совершенно независимое в каждом конкретном случае. Это работает, но с точки зрения теории Информатики считается не очень хорошим решением из-за “жесткой” схемы. О Singleton в Swift можно почитать здесь.

Вариант представлен в приложении CoreData5Swift (Github).

Распространение контекста NSManagedObjectContext методом
UIApplication.sharedApplication().delegate

Это решение обычно применяется в случае использования определенных шаблонов при создании нового проекта, в которых есть опция  “Use Сore Data”. После создания проекта c включенной опцией “Use Сore Data” вы обнаружите огромное количество кода Core Data в AppDelegate, который проделывает все необходимые подготовительные операции для создания контекста managedObjectContext. Поэтому каждый раз, когда вам нужен контекст, вы будете писать строку

(UIApplication.sharedApplication().delegate

                     as? AppDelegate).managedObjectContext

Это тоже Singleton, мы уже знаем как он работает и примеров его использования очень много на разных форумах и обучающих курсах, поэтому мы на нем не будем подробно останавливаться.

Эту статью можно скачать в PDF формате 

Core Data в iOS 9 и Swift при ограничениях на уникальность..pdf

Core Data в iOS 9 и Swift при ограничениях на уникальность.Часть 2.: 2 комментария

    • Ура! Будем переводить. Пока только первая Лекция, тот же калькулятор, но методически преподнесено по-другому.

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