Статьи

Основные данные и Swift: больше NSFetchedResultsController

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

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

Начните с создания нового подкласса 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) {
     
}

Откройте основную раскадровку Main.storyboard , добавьте новый объект контроллера представления и установите для его класса значение UpdateToDoViewController в Identity Inspector . Нажмите Control и перетащите из ячейки прототипа в экземпляре UpdateToDoViewController экземпляр UpdateToDoViewController . Выбрать Выберите Segue> Show в появившемся меню и в инспекторе атрибутов установите для идентификатора SegueUpdateToDoViewController идентификатор.

Добавьте объект UITextField в представление объекта UpdateToDoViewController и настройте его так же, как мы это делали с текстовым полем класса AddToDoViewController . Не забудьте соединить розетку контроллера вида с текстовым полем.

Как и в классе AddToDoViewController , добавьте два элемента кнопок панели на панель навигации контроллера представления, установите для их идентификаторов соответственно Отмена и Сохранить и подключите каждый элемент кнопки панели к соответствующему действию в Инспекторе подключений .

Обновление раскадровки

Нам также нужно внести несколько изменений в класс 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)
}

В 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
    }
}

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

Когда пользователь нажимает кнопку справа от 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()
    }
}

Благодаря классу 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»)
        }
    }
}

Вы можете быть удивлены, почему мы не сохраняем контекст управляемого объекта. Не потеряем ли мы внесенные нами изменения, если не внесем изменения в постоянное хранилище? Да и нет.

Это правда, что в какой-то момент нам нужно записать изменения контекста управляемого объекта в резервное хранилище. Если мы этого не сделаем, пользователь потеряет часть своих данных. Однако нет необходимости сохранять изменения контекста управляемого объекта каждый раз, когда мы вносим изменения.

Лучший подход — сохранить контекст управляемого объекта в тот момент, когда приложение переходит в фоновый режим. Мы можем сделать это в методе 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)»)
    }
}

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