Статьи

Основные данные и Swift: NSFetchedResultsController

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

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

То, что я освещаю в этой серии о Core Data, применимо к iOS 7+ и OS X 10.10+, но основное внимание будет уделено iOS. В этой серии я буду работать с Xcode 7.1 и Swift 2.1. Если вы предпочитаете Objective-C, то я рекомендую прочитать мои предыдущие серии по платформе Core Data .

Откройте Xcode, выберите « Создать»> «Проект …» в меню « Файл» и выберите шаблон приложения « Один вид» в разделе « iOS»> « Категория приложения ».

Выбор шаблона приложения с одним представлением

Назовите проект « Готово» , установите « Язык» в Swift и « Устройства» в iPhone . Поскольку я хотел бы показать вам, как создать приложение Core Data с нуля, убедитесь, что флажок Использовать основные данные снят. Сообщите Xcode, где вы хотите сохранить проект, и нажмите « Создать», чтобы создать проект.

Конфигурирование проекта

Откройте AppDelegate.swift и объявите три ленивых сохраненных свойства managedObjectModel типа NSManagedObjectModel , managedObjectContext типа NSManagedObjectContext и persistentStoreCoordinator типа NSPersistentStoreCoordinator . Если вас смущает этот шаг, то я рекомендую вам вернуться к первой статье этой серии , в которой подробно рассматривается стек основных данных.

01
02
03
04
05
06
07
08
09
10
11
12
13
// MARK: —
// MARK: Core Data Stack
lazy var managedObjectModel: NSManagedObjectModel = {
     
}()
 
lazy var managedObjectContext: NSManagedObjectContext = {
     
}()
 
lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
     
}()

Обратите внимание, что я также добавил оператор импорта для платформы Core Data в верхней части AppDelegate.swift .

1
2
import UIKit
import CoreData

Помните, что мы используем ключевое слово lazy чтобы лениво настроить стек Core Data. Это означает, что мы создаем экземпляр контекста управляемого объекта, модели управляемого объекта и координатора постоянного хранилища в тот момент, когда они нужны приложению. Следующая реализация должна выглядеть очень знакомой.

1
2
3
4
lazy var managedObjectModel: NSManagedObjectModel = {
    let modelURL = NSBundle.mainBundle().URLForResource(«Done», withExtension: «momd»)!
    return NSManagedObjectModel(contentsOfURL: modelURL)!
}()
01
02
03
04
05
06
07
08
09
10
11
lazy var managedObjectContext: NSManagedObjectContext = {
    let persistentStoreCoordinator = self.persistentStoreCoordinator
     
    // Initialize Managed Object Context
    var managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
     
    // Configure Managed Object Context
    managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator
     
    return managedObjectContext
}()
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
    // Initialize Persistent Store Coordinator
    let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
     
    // URL Documents Directory
    let URLs = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
    let applicationDocumentsDirectory = URLs[(URLs.count — 1)]
     
    // URL Persistent Store
    let URLPersistentStore = applicationDocumentsDirectory.URLByAppendingPathComponent(«Done.sqlite»)
     
    do {
        // Add Persistent Store to Persistent Store Coordinator
        try persistentStoreCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: URLPersistentStore, options: nil)
         
    } catch {
        // Populate Error
        var userInfo = [String: AnyObject]()
        userInfo[NSLocalizedDescriptionKey] = «There was an error creating or loading the application’s saved data.»
        userInfo[NSLocalizedFailureReasonErrorKey] = «There was an error creating or loading the application’s saved data.»
         
        userInfo[NSUnderlyingErrorKey] = error as NSError
        let wrappedError = NSError(domain: «com.tutsplus.Done», code: 1001, userInfo: userInfo)
         
        NSLog(«Unresolved error \(wrappedError), \(wrappedError.userInfo)»)
         
        abort()
    }
     
    return persistentStoreCoordinator
}()

Есть три вещи, о которых вы должны знать. Во-первых, модель данных, которую мы создадим дальше, будет называться Done.momd . Во-вторых, мы назовем резервное хранилище Done, и это будет база данных SQLite . В-третьих, если резервное хранилище несовместимо с моделью данных, мы вызываем abort , убивая приложение. Как я упоминал ранее в этой серии, хотя во время разработки это нормально, вам никогда не следует вызывать abort в производственной abort . Мы еще вернемся к проблемам миграции и несовместимости позже в этой серии.

Хотя наше приложение не будет зависать, если вы попытаетесь запустить его, стек основных данных не будет настроен должным образом. Причина проста, мы еще не создали модель данных. Давайте позаботимся об этом на следующем шаге.

Выберите « Создать»> «Файл …» в меню « Файл» и выберите « Модель данных» в категории « iOS»> «Основные данные ».

Создание модели данных

Назовите файл Done , дважды проверьте, что он добавлен в цель Done , и скажите Xcode, где его нужно сохранить.

Наименование модели данных

Модель данных будет очень простой. Создайте новый объект и назовите его Item . Добавьте три атрибута к сущности, имя типа String , созданного с типом Date и выполненного с типом Boolean .

Создание объекта сущности

Отметьте атрибуты как обязательные, а не необязательные, и установите значение по умолчанию для атрибута done как NO .

Конфигурирование атрибута Done

Стек основных данных настроен, а модель данных настроена правильно. Пришло время познакомиться с новым классом инфраструктуры Core Data, классом NSFetchedResultsController .

Эта статья не только о классе NSFetchedResultsController , но и о том, что класс NSFetchedResultsController делает за кулисами. Позвольте мне уточнить, что я имею в виду под этим.

Если бы мы собирали наше приложение без класса NSFetchedResultsController , нам нужно было бы найти способ синхронизации пользовательского интерфейса приложения с данными, управляемыми Core Data. К счастью, у Core Data есть элегантное решение этой проблемы.

Всякий раз, когда запись вставляется , обновляется или удаляется в контексте управляемого объекта, контекст управляемого объекта отправляет уведомление через центр уведомлений. Контекст управляемого объекта отправляет три типа уведомлений:

  • NSManagedObjectContextObjectsDidChangeNotification : это уведомление публикуется каждый раз, когда запись в контексте управляемого объекта вставляется, обновляется или удаляется.
  • NSManagedObjectContextWillSaveNotification : это уведомление публикуется контекстом управляемого объекта до того, как ожидающие изменения фиксируются в резервном хранилище.
  • NSManagedObjectContextDidSaveNotification : это уведомление публикуется контекстом управляемого объекта сразу после ожидающих изменений в резервном хранилище.

Содержимое этих уведомлений идентично, то есть свойство object уведомления — это экземпляр NSManagedObjectContext который разместил уведомление, а словарь userInfo уведомления содержит записи, которые были вставлены , обновлены и удалены .

Суть в том, что для поддержания результатов запроса на получение обновлений требуется значительное количество стандартного кода. Другими словами, если бы мы создавали наше приложение без использования класса NSFetchedResultsController , нам пришлось бы реализовать механизм, который отслеживал бы изменения в контексте управляемого объекта, и соответственно обновлять пользовательский интерфейс. Давайте посмотрим, как NSFetchedResultsController может помочь нам в этой задаче.

Работать с классом NSFetchedResultsController довольно просто. Экземпляр класса NSFetchedResultsController принимает запрос на выборку и имеет объект делегата. Объект NSFetchedResultsController следит за тем, чтобы он NSFetchedResultsController результаты запроса на выборку, отслеживая контекст управляемого объекта, которым был выполнен запрос на выборку.

Если объект NSFetchedResultsController уведомляется о любых изменениях объектом NSManagedObjectContext запроса выборки, он уведомляет своего делегата. Вы можете быть удивлены, как это отличается от контроллера представления, непосредственно контролирующего объект NSManagedObjectContext . NSFetchedResultsController класса NSFetchedResultsController заключается в том, что он обрабатывает уведомления, которые он получает от объекта NSManagedObjectContext и сообщает делегату только то, что ему нужно знать для обновления пользовательского интерфейса в ответ на эти изменения. Методы протокола NSFetchedResultsControllerDelegate должны прояснить это.

1
2
3
4
5
6
7
optional public func controllerWillChangeContent(controller: NSFetchedResultsController)
optional public func controllerDidChangeContent(controller: NSFetchedResultsController)
 
optional public func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType)
optional public func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?)
 
optional public func controller(controller: NSFetchedResultsController, sectionIndexTitleForSectionName sectionName: String) -> String?

Подписи вышеупомянутых методов делегата раскрывают истинное назначение класса NSFetchedResultsController . На iOS класс NSFetchedResultsController был разработан для управления данными, отображаемыми UITableView или UICollectionView . Он сообщает своему делегату, какие именно записи были изменены, как обновить пользовательский интерфейс и когда это сделать.

Не беспокойтесь, если вы все еще не уверены в цели или преимуществах класса NSFetchedResultsController . Это будет иметь больше смысла после того, как мы реализовали протокол NSFetchedResultsControllerDelegate . Давайте NSFetchedResultsController к нашему приложению и NSFetchedResultsController класс NSFetchedResultsController .

Откройте основную раскадровку проекта Main.storyboard , выберите View Controller Scene и внедрите его в контроллер навигации, выбрав « Embed In»> Navigation Controller в меню « Редактор» .

Перетащите объект UITableView в сцену View Controller. создайте выход в классе ViewController и подключите его в раскадровке. Не забудьте ViewController класс ViewController соответствие с протоколами UITableViewDataSource и UITableViewDelegate .

01
02
03
04
05
06
07
08
09
10
11
import UIKit
 
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
 
    @IBOutlet weak var tableView: UITableView!
     
    override func viewDidLoad() {
        super.viewDidLoad()
    }
 
}

Выберите табличное представление, откройте инспектор соединений и подключите источник данных табличного представления и delegate выходы объекту View Controller . С выбранным табличным представлением откройте инспектор атрибутов и установите количество ячеек прототипа равным 1 .

Прежде чем продолжить, нам нужно создать подкласс UITableViewCell для ячейки прототипа. Создайте новый подкласс Objective C, ToDoCell , и установите для его суперкласса UITableViewCell . Создайте два выхода: nameLabel типа UILabel и doneButton типа UIButton .

Вернитесь к раскадровке, выберите ячейку прототипа в табличном представлении и установите класс ToDoCell в Identity Inspector . Добавьте объект UILabel и UIButton в представление содержимого ячейки и подключите выходы в Инспекторе соединений . Выбрав ячейку прототипа, откройте инспектор атрибутов и установите для идентификатора ячейки прототипа ToDoCell . Этот идентификатор будет служить идентификатором повторного использования ячейки. Макет ячейки прототипа должен выглядеть примерно так, как показано на скриншоте ниже.

Создание ячейки прототипа ToDoCell

Создайте новый подкласс ViewController и назовите его AddToDoViewController . Откройте AddToDoViewController.swift , объявите выходное textField типа UITextField и UITextField контроллер представления в UITextFieldDelegate протоколом UITextFieldDelegate .

01
02
03
04
05
06
07
08
09
10
11
import UIKit
 
class AddToDoViewController: UIViewController, UITextFieldDelegate {
 
    @IBOutlet weak var textField: UITextField!
     
    override func viewDidLoad() {
        super.viewDidLoad()
    }
 
}

Прежде чем добавить контроллер представления в раскадровку, добавьте следующие два действия в файл реализации контроллера представления.

1
2
3
4
5
6
7
8
9
// MARK: —
// MARK: Actions
@IBAction func cancel(sender: AnyObject) {
     
}
 
@IBAction func save(sender: AnyObject) {
     
}

Откройте раскадровку еще раз и добавьте элемент кнопки панели с идентификатором Add справа от панели навигации ViewController . Добавьте новый контроллер представления в раскадровку и установите в его классе AddToDoViewController в Identity Inspector . Выбрав контроллер представления, выберите « Встроить»> «Контроллер навигации» в меню « Редактор» .

Новый контроллер представления теперь должен иметь панель навигации. Добавьте два элемента панели кнопок на панель навигации, один слева с идентификатором Отмена и один справа с идентификатором Сохранить . Соедините действие cancel(_:) с элементом кнопки левой панели, а действие save(_:) — с элементом кнопки правой панели.

Добавьте объект UITextField в представление контроллера представления и UITextField его на 20 пунктов ниже панели навигации. Текстовое поле должно оставаться в 20 точках ниже панели навигации. Обратите внимание, что ограничение макета в верхней части ссылается на руководство по верхнему макету .

Добавление ограничений макета

Соедините текстовое поле с соответствующим выходом в контроллере представления и установите контроллер представления в качестве делегата текстового поля. Наконец, перетащите элемент управления с ViewController панели кнопок ViewController на контроллер навигации, для которого AddToDoViewController является корневым контроллером представления. Установите для типа segue значение Present Modally и установите для идентификатора SegueAddToDoViewController в Инспекторе атрибутов . Это было много, чтобы принять. Интересная часть еще впереди, хотя.

Прежде чем мы сможем принять наше приложение для вращения, нам нужно реализовать протокол UITableViewDataSource в классе ViewController . Однако именно NSFetchedResultsController вступает в игру класс NSFetchedResultsController . Чтобы убедиться, что все работает, верните 0 из tableView(_:numberOfRowsInSection:) . Это приведет к пустому табличному представлению, но позволит запустить приложение без сбоев.

1
2
3
4
5
// MARK: —
// MARK: Table View Data Source Methods
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return 0
}

Чтобы удовлетворить компилятор, нам также нужно реализовать tableView(_:cellForRowAtIndexPath:) . В верхней части AddToDoViewController.swift добавьте константу для идентификатора повторного использования ячейки.

1
2
3
4
5
6
7
8
import UIKit
 
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
 
    let ReuseIdentifierToDoCell = «ToDoCell»
 
    …
}

Реализация tableView(_:cellForRowAtIndexPath:) довольно проста, поскольку мы пока не делаем ничего особенного с ячейкой табличного представления.

1
2
3
4
5
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier(ReuseIdentifierToDoCell, forIndexPath: indexPath) as!
     
    return cell
}

Откройте класс AddToDoViewController и реализуйте методы cancel(_:) и save(_:) как показано ниже. Мы обновим их реализации позже в этом уроке.

1
2
3
4
5
6
7
8
9
// MARK: —
// MARK: Actions
@IBAction func cancel(sender: AnyObject) {
    dismissViewControllerAnimated(true, completion: nil)
}
 
@IBAction func save(sender: AnyObject) {
    dismissViewControllerAnimated(true, completion: nil)
}

Создайте и запустите приложение в симуляторе, чтобы увидеть, все ли правильно подключено. Вы должны быть в состоянии нажать кнопку добавления в правом верхнем углу, чтобы вызвать AddToDoViewController и закрыть его, нажав либо кнопку отмены, либо кнопку сохранения.

Класс NSFetchedResultsController является частью инфраструктуры Core Data и предназначен для управления результатами запроса на выборку. Класс был разработан для бесперебойной работы с UITableView и UICollectionView на iOS и NSTableView на OS X. Однако его можно использовать и для других целей.

Однако прежде чем мы сможем начать работу с классом NSFetchedResultsController класс ViewController должен получить доступ к экземпляру NSManagedObjectContext экземпляру NSManagedObjectContext мы создали ранее в ViewController приложения. Начните с объявления свойства managedObjectContext типа NSManagedObjectContext! в заголовочном файле класса ViewController . Обратите внимание, что мы также добавляем оператор импорта для базовой структуры данных вверху.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
import UIKit
import CoreData
 
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
 
    let ReuseIdentifierToDoCell = «ToDoCell»
     
    @IBOutlet weak var tableView: UITableView!
     
    var managedObjectContext: NSManagedObjectContext!
 
    …
 
}

Откройте Main.storyboard , выберите начальный контроллер представления раскадровки, экземпляр UINavigationController и установите его Storyboard ID StoryboardIDRootNavigationController в Инспекторе удостоверений .

В приложении делегата application(_:didFinishLaunchingWithOptions:) метод application(_:didFinishLaunchingWithOptions:) мы получаем ссылку на экземпляр ViewController , корневой контроллер представления контроллера навигации, и устанавливаем его свойство managedObjectContext . Обновленное application(_:didFinishLaunchingWithOptions:) выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    // Fetch Main Storyboard
    let mainStoryboard = UIStoryboard(name: «Main», bundle: nil)
     
    // Instantiate Root Navigation Controller
    let rootNavigationController = mainStoryboard.instantiateViewControllerWithIdentifier(«StoryboardIDRootNavigationController») as!
     
    // Configure View Controller
    let viewController = rootNavigationController.topViewController as?
     
    if let viewController = viewController {
        viewController.managedObjectContext = self.managedObjectContext
    }
     
    // Configure Window
    window?.rootViewController = rootNavigationController
     
    return true
}

Чтобы убедиться, что все работает, добавьте следующую инструкцию print в метод viewDidLoad() класса ViewController .

1
2
3
4
5
override func viewDidLoad() {
    super.viewDidLoad()
     
    print(managedObjectContext)
}

Откройте файл ViewController класса ViewController и объявите ленивое сохраненное свойство типа NSFetchedResultsController . Назовите свойство fetchedResultsController . Экземпляр NSFetchedResultsController также имеет свойство делегата, которое должно соответствовать протоколу NSFetchedResultsControllerDelegate . Поскольку экземпляр ViewController будет служить делегатом экземпляра NSFetchedResultsController , нам необходимо согласовать класс NSFetchedResultsControllerDelegate протоколом NSFetchedResultsControllerDelegate , как показано ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
import UIKit
import CoreData
 
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, NSFetchedResultsControllerDelegate {
 
    let ReuseIdentifierToDoCell = «ToDoCell»
     
    @IBOutlet weak var tableView: UITableView!
     
    var managedObjectContext: NSManagedObjectContext!
     
    var fetchedResultsController: NSFetchedResultsController = {
         
    }()
 
    …
 
}

Пришло время инициализировать экземпляр NSFetchedResultsController . Сердцем контроллера полученных результатов является объект NSFetchRequest , поскольку он определяет, какими записями будет управлять контроллер полученных результатов. В viewDidLoad() контроллера представления мы инициализируем запрос на выборку, передавая "Item" методу initWithEntityName(_:) . Это должно быть уже знакомо, и поэтому следующая строка, в которой мы добавляем дескрипторы сортировки в запрос на выборку, сортирует результаты на основе значения атрибута createAt каждой записи.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
lazy var fetchedResultsController: NSFetchedResultsController = {
    // Initialize Fetch Request
    let fetchRequest = NSFetchRequest(entityName: «Item»)
     
    // Add Sort Descriptors
    let sortDescriptor = NSSortDescriptor(key: «createdAt», ascending: true)
    fetchRequest.sortDescriptors = [sortDescriptor]
     
    // Initialize Fetched Results Controller
    let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)
     
    // Configure Fetched Results Controller
    fetchedResultsController.delegate = self
     
    return fetchedResultsController
}()

Инициализация контроллера полученных результатов довольно проста. Метод init(fetchRequest:managedObjectContext:sectionNameKeyPath:cacheName:) принимает четыре аргумента:

  • запрос на получение
  • контекст управляемого объекта, который контролирует выбранный контроллер результатов
  • путь ключа раздела, если вы хотите, чтобы результаты были разделены на разделы
  • имя кэша, если вы хотите включить кэширование

Мы передаем nil для двух последних параметров на данный момент. Первый аргумент очевиден, но зачем нам передавать объект NSManagedObjectContext ? Мало того, что переданный в контексте управляемого объекта используется для выполнения запроса выборки, это также контекст управляемого объекта, за которым контроллер полученных результатов будет следить за изменениями. Это станет яснее через несколько минут, когда мы начнем реализовывать методы NSFetchedResultsControllerDelegate протокола NSFetchedResultsControllerDelegate .

Наконец, нам нужно указать контроллеру полученных результатов выполнить запрос на выборку, который мы передали. Мы делаем это, вызывая performFetch() в viewDidLoad() . Обратите внимание, что это метод throwing, что означает, что нам нужно заключить его в оператор do-catch . Метод performFetch() аналогичен executeFetchRequest(_:) класса NSManagedObjectContext .

01
02
03
04
05
06
07
08
09
10
override func viewDidLoad() {
    super.viewDidLoad()
     
    do {
        try self.fetchedResultsController.performFetch()
    } catch {
        let fetchError = error as NSError
        print(«\(fetchError), \(fetchError.userInfo)»)
    }
}

Когда настроенный контроллер результатов настроен и готов к использованию, нам необходимо реализовать протокол NSFetchedResultsControllerDelegate . Как мы видели ранее, протокол определяет пять методов, три из которых представляют интерес для нас в этом руководстве:

  • controllerWillChangeContent(controller: NSFetchedResultsController)
  • controllerDidChangeContent(controller: NSFetchedResultsController)
  • controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?)

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

В нашем примере это сводится к следующим реализациям controllerWillChangeContent(controller: NSFetchedResultsController) и controllerDidChangeContent(controller: NSFetchedResultsController) .

1
2
3
4
5
6
7
8
9
// MARK: —
// MARK: Fetched Results Controller Delegate Methods
func controllerWillChangeContent(controller: NSFetchedResultsController) {
    tableView.beginUpdates()
}
 
func controllerDidChangeContent(controller: NSFetchedResultsController) {
    tableView.endUpdates()
}

Реализация controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) Является битовой. Этот метод делегата принимает не менее пяти аргументов:

  • экземпляр NSFetchedResultsController
  • экземпляр NSManagedObject который изменился
  • текущий путь индекса записи в контроллере полученных результатов
  • тип изменения, то есть вставка , обновление или удаление
  • новый индексный путь записи в контроллере полученных результатов после изменения

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

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

Реализация controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) Выглядит сложной, но controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?)

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
    switch (type) {
    case .Insert:
        if let indexPath = newIndexPath {
            tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
        }
        break;
    case .Delete:
        if let indexPath = indexPath {
            tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
        }
        break;
    case .Update:
        if let indexPath = indexPath {
            let cell = tableView.cellForRowAtIndexPath(indexPath) as!
            configureCell(cell, atIndexPath: indexPath)
        }
        break;
    case .Move:
        if let indexPath = indexPath {
            tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
        }
         
        if let newIndexPath = newIndexPath {
            tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)
        }
        break;
    }
}

Тип изменения имеет тип NSFetchedResultsChangeType . Это перечисление имеет четыре значения элемента:

  • Insert
  • Delete
  • Move
  • Update

Имена довольно понятны. Если типом является Insert , мы сообщаем табличному представлению вставить строку в newIndexPath . Точно так же, если тип — Delete , мы удаляем строку в indexPath из табличного представления.

Если запись обновляется, мы обновляем соответствующую строку в табличном представлении, вызывая configureCell(_:atIndexPath:) , вспомогательный метод, который принимает объект NSIndexPath объект NSIndexPath . Мы реализуем этот метод в ближайшее время.

Если тип изменения равен Move , мы удаляем строку в indexPath и вставляем строку в newIndexPath чтобы отразить обновленную позицию записи во внутренней структуре данных newIndexPath контроллера результатов.

Это было не слишком сложно. Это было? Реализация протокола UITableViewDataSource намного проще, но есть несколько вещей, о которых вы должны знать. Давайте начнем с numberOfSectionsInTableView(_:) и tableView(_:numberOfRowsInSection:

Несмотря на то, что табличное представление в нашем примере приложения будет иметь только один раздел, давайте спросим у контроллера полученных результатов о количестве разделов. Мы делаем это, вызывая sections , которые возвращают массив объектов, которые соответствуют протоколу NSFetchedResultsSectionInfo .

Объекты, соответствующие протоколу NSFetchedResultsSectionInfo , должны реализовать несколько методов и свойств, включая numberOfObjects . Это дает нам то, что нам нужно для реализации первых двух методов протокола UITableViewDataSource .

1
2
3
4
5
6
7
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    if let sections = fetchedResultsController.sections {
        return sections.count
    }
     
    return 0
}
1
2
3
4
5
6
7
8
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    if let sections = fetchedResultsController.sections {
        let sectionInfo = sections[section]
        return sectionInfo.numberOfObjects
    }
     
    return 0
}

Далее идут tableView(_:cellForRowAtIndexPath:) и configureCell(_:atIndexPath:) . Реализация tableView(_:cellForRowAtIndexPath:) коротка, потому что мы переместили большую часть конфигурации ячейки в configureCell(_:atIndexPath:) . Мы просим табличное представление для повторно используемой ячейки с идентификатором повторного использования ReuseIdentifierToDoCell и передаем ячейку и путь индекса в configureCell(_:atIndexPath:) .

1
2
3
4
5
6
7
8
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier(ReuseIdentifierToDoCell, forIndexPath: indexPath) as!
     
    // Configure Table View Cell
    configureCell(cell, atIndexPath: indexPath)
     
    return cell
}

Волшебство происходит в configureCell(_:atIndexPath:) . Мы просим контроллер полученных результатов для элемента в indexPath . Контроллер полученных результатов возвращает нам экземпляр NSManagedObject . Мы обновляем nameLabel и состояние doneButton , запрашивая у записи его имя и атрибуты done .

01
02
03
04
05
06
07
08
09
10
11
12
13
func configureCell(cell: ToDoCell, atIndexPath indexPath: NSIndexPath) {
    // Fetch Record
    let record = fetchedResultsController.objectAtIndexPath(indexPath)
     
    // Update Cell
    if let name = record.valueForKey(«name») as?
        cell.nameLabel.text = name
    }
     
    if let done = record.valueForKey(«done») as?
        cell.doneButton.selected = done
    }
}

Мы вернемся к протоколу UITableViewDataSource позже в этом руководстве, когда будем удалять элементы из списка. Сначала нам нужно заполнить табличное представление некоторыми данными.

Давайте закончим этот урок, добавив возможность создавать задачи. Откройте класс AddToDoViewController , добавьте оператор импорта для платформы Core Data и объявите свойство managedObjectContext типа NSManagedObjectContext! ,

01
02
03
04
05
06
07
08
09
10
11
12
import UIKit
import CoreData
 
class AddToDoViewController: UIViewController, UITextFieldDelegate {
 
    @IBOutlet weak var textField: UITextField!
     
    var managedObjectContext: NSManagedObjectContext!
 
    …
 
}

Вернитесь к классу ViewController и реализуйте метод prepareForSegue(_:sender:) . В этом методе мы устанавливаем свойство managedObjectContext экземпляра AddToDoViewController . Если вы раньше работали с раскадровками, то реализация prepareForSegue(_:sender:) должна быть простой.

01
02
03
04
05
06
07
08
09
10
11
// MARK: —
// MARK: Prepare for Segue
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if segue.identifier == «SegueAddToDoViewController» {
        if let navigationController = segue.destinationViewController as?
            if let viewController = navigationController.topViewController as?
                viewController.managedObjectContext = managedObjectContext
            }
        }
    }
}

Если пользователь вводит текст в текстовое поле AddToDoViewController и нажимает кнопку « Сохранить» , мы создаем новую запись, заполняем ее данными и сохраняем ее. Эта логика входит в метод save(_:) который мы создали ранее.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@IBAction func save(sender: AnyObject) {
    let name = textField.text
     
    if let isEmpty = name?.isEmpty where isEmpty == false {
        // Create Entity
        let entity = NSEntityDescription.entityForName(«Item», inManagedObjectContext: self.managedObjectContext)
         
        // Initialize Record
        let record = NSManagedObject(entity: entity!, insertIntoManagedObjectContext: self.managedObjectContext)
         
        // Populate Record
        record.setValue(name, forKey: «name»)
        record.setValue(NSDate(), forKey: «createdAt»)
         
        do {
            // Save Record
            try record.managedObjectContext?.save()
             
            // Dismiss View Controller
            dismissViewControllerAnimated(true, completion: nil)
             
        } catch {
            let saveError = error as NSError
            print(«\(saveError), \(saveError.userInfo)»)
             
            // Show Alert View
            showAlertWithTitle(«Warning», message: «Your to-do could not be saved.», cancelButtonTitle: «OK»)
        }
         
    } else {
        // Show Alert View
        showAlertWithTitle(«Warning», message: «Your to-do needs a name.», cancelButtonTitle: «OK»)
    }
}

Метод save(_:) выглядит довольно впечатляюще, но там нет ничего, что мы еще не рассмотрели. Мы создаем новый управляемый объект, используя экземпляр NSEntityDescription экземпляр NSManagedObjectContext . Затем мы заполняем управляемый объект именем и датой.

Если сохранение контекста управляемого объекта прошло успешно, мы отклоняем контроллер представления, в противном случае мы показываем предупреждение, вызывая showAlertWithTitle(_:message:cancelButtonTitle:) , вспомогательный метод. Если пользователь нажимает кнопку сохранения без ввода текста, мы также показываем предупреждение. В showAlertWithTitle(_:message:cancelButtonTitle:) мы создаем, настраиваем и представляем UIAlertController .

01
02
03
04
05
06
07
08
09
10
11
12
// MARK: —
// MARK: Helper Methods
private func showAlertWithTitle(title: String, message: String, cancelButtonTitle: String) {
    // Initialize Alert Controller
    let alertController = UIAlertController(title: title, message: message, preferredStyle: .Alert)
     
    // Configure Alert Controller
    alertController.addAction(UIAlertAction(title: cancelButtonTitle, style: .Default, handler: nil))
     
    // Present Alert Controller
    presentViewController(alertController, animated: true, completion: nil)
}

Запустите приложение и добавьте несколько элементов. Я уверен, что вы согласны с тем, что класс NSFetchedResultsController делает процесс добавления элементов невероятно простым. Он заботится о мониторинге контекста управляемого объекта на предмет изменений, и мы обновляем пользовательский интерфейс, табличное представление класса ViewController , основываясь на том, что экземпляр NSFetchedResultsController сообщает нам через протокол NSFetchedResultsControllerDelegate .

В следующей статье мы закончим приложение, добавив возможность удалять и обновлять элементы списка дел. Важно, чтобы вы поняли концепции, которые мы обсуждали в этой статье. Способ, которым Core Data транслирует изменения состояния контекста управляемого объекта, имеет важное значение, поэтому убедитесь, что вы это понимаете, прежде чем двигаться дальше.