В этом руководстве мы продолжим исследование класса 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, чтобы узнать, что еще он может сделать для вас.