В этом руководстве мы продолжим исследование класса NSFetchedResultsController , добавив возможность обновлять и удалять элементы NSFetchedResultsController дел. Вы заметите, что обновление и удаление текущих дел на удивление легко благодаря основам, которые мы заложили в предыдущем уроке .
Предпосылки
То, что я освещаю в этой серии о Core Data, применимо к iOS 7+ и OS X 10.10+, но основное внимание будет уделено iOS. В этой серии я буду работать с Xcode 7.1 и Swift 2.1. Если вы предпочитаете Objective-C, то я рекомендую прочитать мои предыдущие серии по платформе Core Data .
1. Обновление имени записи
Шаг 1: Создать View Controller
Начните с создания нового подкласса UpdateToDoViewController именем UpdateToDoViewController . В UpdateToDoViewController.swift объявите выход textField типа UITextField! и два свойства managedObjectContext типа NSManagedObjectContext! и record типа NSManagedObject! , Добавьте оператор импорта для базовой структуры данных вверху.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
|
import UIKit
import CoreData
class UpdateToDoViewController: UIViewController {
@IBOutlet weak var textField: UITextField!
var record: NSManagedObject!
var managedObjectContext: NSManagedObjectContext!
…
}
|
Затем создайте два действия: cancel(_:) и save(_:) . Их реализации могут пока оставаться пустыми.
|
1
2
3
4
5
6
7
8
9
|
// MARK: —
// MARK: Actions
@IBAction func cancel(send: AnyObject) {
}
@IBAction func save(send: AnyObject) {
}
|
Шаг 2: Обновить раскадровку
Откройте основную раскадровку Main.storyboard , добавьте новый объект контроллера представления и установите для его класса значение UpdateToDoViewController в Identity Inspector . Нажмите Control и перетащите из ячейки прототипа в экземпляре UpdateToDoViewController экземпляр UpdateToDoViewController . Выбрать Выберите Segue> Show в появившемся меню и в инспекторе атрибутов установите для идентификатора SegueUpdateToDoViewController идентификатор.
Добавьте объект UITextField в представление объекта UpdateToDoViewController и настройте его так же, как мы это делали с текстовым полем класса AddToDoViewController . Не забудьте соединить розетку контроллера вида с текстовым полем.
Как и в классе AddToDoViewController , добавьте два элемента кнопок панели на панель навигации контроллера представления, установите для их идентификаторов соответственно Отмена и Сохранить и подключите каждый элемент кнопки панели к соответствующему действию в Инспекторе подключений .

Шаг 3: Передача ссылки
Нам также нужно внести несколько изменений в класс ViewController . Начнем с обновления prepareForSegue(_:sender:) , мы выбираем запись, соответствующую выбору пользователя, и передаем ее в экземпляр UpdateToDoViewController .
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// 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
}
}
} else if segue.identifier == «SegueUpdateToDoViewController» {
if let viewController = segue.destinationViewController as?
if let indexPath = tableView.indexPathForSelectedRow {
// Fetch Record
let record = fetchedResultsController.objectAtIndexPath(indexPath) as!
// Configure View Controller
viewController.record = record
viewController.managedObjectContext = managedObjectContext
}
}
}
}
|
Чтобы закончить, мы реализуем метод tableView(_:didSelectRowAtIndexPath:) протокола UITableViewDelegate . В этом методе мы отменяем выбор строки, которую коснулся пользователь.
|
1
2
3
4
5
|
// MARK: —
// MARK: Table View Delegate Methods
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: true)
}
|
Шаг 4: Заполнение текстового поля
В viewDidLoad() класса UpdateToDoViewController заполните текстовое поле именем записи, как показано ниже.
|
1
2
3
4
5
6
7
8
9
|
// MARK: —
// MARK: View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
if let name = record.valueForKey(«name») as?
textField.text = name
}
}
|
Шаг 5: Обновление записи
В действии cancel(_:) мы извлекаем контроллер обновлений из стека навигации контроллера навигации.
|
1
2
3
|
@IBAction func cancel(send: AnyObject) {
navigationController?.popViewControllerAnimated(true)
}
|
В действии save(_:) мы сначала проверяем, является ли текстовое поле пустым, и показываем предупреждение, если оно есть. Если текстовое поле содержит допустимое значение, мы обновляем атрибут name записи и извлекаем контроллер представления из стека навигации контроллера навигации.
|
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
|
@IBAction func save(sender: AnyObject) {
let name = textField.text
if let isEmpty = name?.isEmpty where isEmpty == false {
// Update Record
record.setValue(name, forKey: «name»)
do {
// Save Record
try record.managedObjectContext?.save()
// Dismiss View Controller
navigationController?.popViewControllerAnimated(true)
} 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»)
}
}
|
Реализация showAlertWithTitle(_:message:cancelButtonTitle:) идентична AddToDoViewController класса AddToDoViewController .
|
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)
}
|
Это все, что нужно для обновления записи с использованием Core Data. Запустите приложение, чтобы убедиться, что все работает. Контроллер полученных результатов автоматически обнаруживает изменение и уведомляет его делегата, экземпляр ViewController . Объект ViewController , в свою очередь, обновляет табличное представление, чтобы отразить изменение. Это так просто.
2. Обновление состояния записи
Шаг 1: Обновление ToDoCell
Когда пользователь нажимает кнопку справа от ToDoCell , состояние элемента должно измениться. Для этого нам сначала нужно обновить класс ToDoCell . Откройте ToDoCell.swift и добавьте typealias для обработчика с именем ToDoCellDidTapButtonHandler . Затем объявите свойство didTapButtonHandler типа ToDoCellDidTapButtonHandler? ,
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
import UIKit
typealias ToDoCellDidTapButtonHandler = () -> ()
class ToDoCell: UITableViewCell {
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var doneButton: UIButton!
var didTapButtonHandler: ToDoCellDidTapButtonHandler?
…
}
|
В awakeFromNib() класса awakeFromNib() мы вызываем вспомогательный метод setupView() для настройки ячейки табличного представления.
|
1
2
3
4
5
6
7
|
// MARK: —
// MARK: Initialization
override func awakeFromNib() {
super.awakeFromNib()
setupView()
}
|
В setupView() мы настраиваем объект doneButton , устанавливая изображения для каждого состояния кнопки и добавляя ячейку табличного представления в качестве цели. Когда пользователь нажимает кнопку, ячейке табличного представления отправляется сообщение didTapButton(_:) в котором мы didTapButtonHandler замыкание didTapButtonHandler . Через мгновение вы увидите, насколько удобен этот шаблон. Изображения включены в исходные файлы этого урока, которые вы можете найти на GitHub .
|
01
02
03
04
05
06
07
08
09
10
11
12
|
// MARK: —
// MARK: View Methods
private func setupView() {
let imageNormal = UIImage(named: «button-done-normal»)
let imageSelected = UIImage(named: «button-done-selected»)
doneButton.setImage(imageNormal, forState: .Normal)
doneButton.setImage(imageNormal, forState: .Disabled)
doneButton.setImage(imageSelected, forState: .Selected)
doneButton.setImage(imageSelected, forState: .Highlighted)
doneButton.addTarget(self, action: «didTapButton:», forControlEvents: .TouchUpInside)
}
|
|
1
2
3
4
5
6
7
|
// MARK: —
// MARK: Actions
func didTapButton(sender: AnyObject) {
if let handler = didTapButtonHandler {
handler()
}
}
|
Шаг 2: Обновление ViewController
Благодаря классу NSFetchedResultsController и заложенному нами фундаменту нам нужно только обновить метод configureCell(_:atIndexPath:) в классе ViewController .
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
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
}
cell.didTapButtonHandler = {
if let done = record.valueForKey(«done») as?
record.setValue(!done, forKey: «done»)
}
}
}
|
Шаг 3: Сохранение изменений
Вы можете быть удивлены, почему мы не сохраняем контекст управляемого объекта. Не потеряем ли мы внесенные нами изменения, если не внесем изменения в постоянное хранилище? Да и нет.
Это правда, что в какой-то момент нам нужно записать изменения контекста управляемого объекта в резервное хранилище. Если мы этого не сделаем, пользователь потеряет часть своих данных. Однако нет необходимости сохранять изменения контекста управляемого объекта каждый раз, когда мы вносим изменения.
Лучший подход — сохранить контекст управляемого объекта в тот момент, когда приложение переходит в фоновый режим. Мы можем сделать это в методе applicationDidEnterBackground(_:) протокола UIApplicationDelegate . Откройте AppDelegate.swift и реализуйте applicationDidEnterBackground(_:) как показано ниже.
|
1
2
3
4
5
6
7
8
|
func applicationDidEnterBackground(application: UIApplication) {
do {
try self.managedObjectContext.save()
} catch {
let saveError = error as NSError
print(«\(saveError), \(saveError.userInfo)»)
}
}
|
Однако это не работает, если пользователь принудительно завершает приложение. Поэтому хорошей идеей также является сохранение контекста управляемого объекта при завершении работы приложения. Метод applicationWillTerminate(_:) — это другой метод протокола UIApplicationDelegate , который уведомляет делегата приложения о том, что приложение должно быть завершено.
|
1
2
3
4
5
6
7
8
|
func applicationWillTerminate(application: UIApplication) {
do {
try self.managedObjectContext.save()
} catch {
let saveError = error as NSError
print(«\(saveError), \(saveError.userInfo)»)
}
}
|
Обратите внимание, что у нас есть повторяющийся код в applicationDidEnterBackground(_:) и applicationWillTerminate(_:) . Давайте проявим смекалку и создадим вспомогательный метод для сохранения контекста управляемого объекта и вызовем этот вспомогательный метод в обоих методах делегата.
|
01
02
03
04
05
06
07
08
09
10
|
// MARK: —
// MARK: Helper Methods
private func saveManagedObjectContext() {
do {
try self.managedObjectContext.save()
} catch {
let saveError = error as NSError
print(«\(saveError), \(saveError.userInfo)»)
}
}
|
3. Удаление записей
Вы будете удивлены тем, сколько строк потребуется для удаления записей с использованием класса NSFetchedResultsController . Начните с реализации tableView(_:canEditRowAtIndexPath:) протокола UITableViewDataSource .
|
1
2
3
|
func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
return true
}
|
Второй метод протокола UITableViewDataSource , который нам нужно реализовать, — это tableView(_:commitEditingStyle:forRowAtIndexPath:) . В этом методе мы выбираем управляемый объект, выбранный пользователем для удаления, и передаем его в метод deleteObject(_:) контекста управляемого объекта контроллера извлеченных результатов.
|
1
2
3
4
5
6
7
8
9
|
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if (editingStyle == .Delete) {
// Fetch Record
let record = fetchedResultsController.objectAtIndexPath(indexPath) as!
// Delete Record
managedObjectContext.deleteObject(record)
}
}
|
Поскольку мы уже реализовали протокол NSFetchedResultsControllerDelegate , пользовательский интерфейс автоматически обновляется, включая анимацию.
Вывод
Я надеюсь, что вы согласны с тем, что класс NSFetchedResultsController является очень удобным членом инфраструктуры Core Data. Если вы понимаете основы инфраструктуры базовых данных, то освоить этот класс несложно. Я призываю вас дополнительно изучить его API, чтобы узнать, что еще он может сделать для вас.