В предыдущих выпусках этой серии мы рассмотрели основы инфраструктуры основных данных. Пришло время использовать наши знания, создав простое приложение на базе 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 .
1. Настройка проекта
Откройте Xcode, выберите « Создать»> «Проект …» в меню « Файл» и выберите шаблон приложения « Один вид» в разделе « iOS»> « Категория приложения ».
Назовите проект « Готово» , установите « Язык» в Swift и « Устройства» в iPhone . Поскольку я хотел бы показать вам, как создать приложение Core Data с нуля, убедитесь, что флажок Использовать основные данные снят. Сообщите Xcode, где вы хотите сохранить проект, и нажмите « Создать», чтобы создать проект.
2. Настройка основных данных
Откройте 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
. Мы еще вернемся к проблемам миграции и несовместимости позже в этой серии.
Хотя наше приложение не будет зависать, если вы попытаетесь запустить его, стек основных данных не будет настроен должным образом. Причина проста, мы еще не создали модель данных. Давайте позаботимся об этом на следующем шаге.
3. Создание модели данных
Выберите « Создать»> «Файл …» в меню « Файл» и выберите « Модель данных» в категории « iOS»> «Основные данные ».
Назовите файл Done , дважды проверьте, что он добавлен в цель Done , и скажите Xcode, где его нужно сохранить.
Модель данных будет очень простой. Создайте новый объект и назовите его Item . Добавьте три атрибута к сущности, имя типа String , созданного с типом Date и выполненного с типом Boolean .
Отметьте атрибуты как обязательные, а не необязательные, и установите значение по умолчанию для атрибута done как NO
.
Стек основных данных настроен, а модель данных настроена правильно. Пришло время познакомиться с новым классом инфраструктуры Core Data, классом NSFetchedResultsController
.
4. Управление данными
Эта статья не только о классе NSFetchedResultsController
, но и о том, что класс NSFetchedResultsController
делает за кулисами. Позвольте мне уточнить, что я имею в виду под этим.
Если бы мы собирали наше приложение без класса NSFetchedResultsController
, нам нужно было бы найти способ синхронизации пользовательского интерфейса приложения с данными, управляемыми Core Data. К счастью, у Core Data есть элегантное решение этой проблемы.
Всякий раз, когда запись вставляется , обновляется или удаляется в контексте управляемого объекта, контекст управляемого объекта отправляет уведомление через центр уведомлений. Контекст управляемого объекта отправляет три типа уведомлений:
-
NSManagedObjectContextObjectsDidChangeNotification
: это уведомление публикуется каждый раз, когда запись в контексте управляемого объекта вставляется, обновляется или удаляется. -
NSManagedObjectContextWillSaveNotification
: это уведомление публикуется контекстом управляемого объекта до того, как ожидающие изменения фиксируются в резервном хранилище. -
NSManagedObjectContextDidSaveNotification
: это уведомление публикуется контекстом управляемого объекта сразу после ожидающих изменений в резервном хранилище.
Содержимое этих уведомлений идентично, то есть свойство object
уведомления — это экземпляр NSManagedObjectContext
который разместил уведомление, а словарь userInfo
уведомления содержит записи, которые были вставлены , обновлены и удалены .
Суть в том, что для поддержания результатов запроса на получение обновлений требуется значительное количество стандартного кода. Другими словами, если бы мы создавали наше приложение без использования класса NSFetchedResultsController
, нам пришлось бы реализовать механизм, который отслеживал бы изменения в контексте управляемого объекта, и соответственно обновлять пользовательский интерфейс. Давайте посмотрим, как NSFetchedResultsController
может помочь нам в этой задаче.
5. Настройка интерфейса пользователя
Работать с классом 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
.
Шаг 1: Заполнение раскадровки
Откройте основную раскадровку проекта 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
. Этот идентификатор будет служить идентификатором повторного использования ячейки. Макет ячейки прототипа должен выглядеть примерно так, как показано на скриншоте ниже.
Создайте новый подкласс 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
в Инспекторе атрибутов . Это было много, чтобы принять. Интересная часть еще впереди, хотя.
Шаг 2: Реализация табличного представления
Прежде чем мы сможем принять наше приложение для вращения, нам нужно реализовать протокол 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
}
|
Шаг 3: Сохранить и отменить
Откройте класс 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
и закрыть его, нажав либо кнопку отмены, либо кнопку сохранения.
6. Добавление класса NSFetchedResultsController
Класс NSFetchedResultsController
является частью инфраструктуры Core Data и предназначен для управления результатами запроса на выборку. Класс был разработан для бесперебойной работы с UITableView
и UICollectionView
на iOS и NSTableView
на OS X. Однако его можно использовать и для других целей.
Шаг 1: заложить основу
Однако прежде чем мы сможем начать работу с классом 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)
}
|
Шаг 2. Инициализация экземпляра NSFetchedResultsController
Откройте файл 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)»)
}
}
|
Шаг 3: Реализация протокола делегата
Когда настроенный контроллер результатов настроен и готов к использованию, нам необходимо реализовать протокол 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
контроллера результатов.
Шаг 4: Реализация протокола UITableViewDataSource
Это было не слишком сложно. Это было? Реализация протокола 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
позже в этом руководстве, когда будем удалять элементы из списка. Сначала нам нужно заполнить табличное представление некоторыми данными.
7. Добавление записей
Давайте закончим этот урок, добавив возможность создавать задачи. Откройте класс 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 транслирует изменения состояния контекста управляемого объекта, имеет важное значение, поэтому убедитесь, что вы это понимаете, прежде чем двигаться дальше.