Core Data + Swift для самых маленьких: необходимый минимум (часть 3)
В этой статье мы повернемся лицом к пользователю и поработаем над интерфейсной частью, помогать нам в этом будет NSFetchRequest и NSFetchedResultsController. Данная часть получилась довольно большой, но я не вижу смысла дробить ее на несколько публикаций. Аккуратнее, под катом много кода и картинок.
Интерфейс — вещь неоднозначная и, в зависимости от требования к продукту, может существенное меняться. В данной статье я не буду уделять ему слишком много времени, точнее говоря, буду уделять совсем мало (я имею ввиду следование Guidelines и тому подобное). Моя задача в данной части статьи состоит в том, чтобы показать, как Core Data
может очень органично вписаться в элементы управления iOS. Поэтому я буду использовать для этих целей такой интерфейс, при использовании которого взаимодействие элементов управления и Core Data
будет выглядеть проще и нагляднее. Очевидно, что в реальном приложении интерфейсной части надо будет посвятить гораздо больше времени.
Справочники
Прежде чем начать, давайте придадим модулю делегата приложения (
AppDelegate.swift
), в котором мы экспериментировали в прошлой части статьи, первоначальный вид. // AppDelegate.swift
// core-data-habrahabr-swift
import UIKit
import CoreData
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
return true
}
func applicationWillTerminate(application: UIApplication) {
CoreDataManager.instance.saveContext()
}
}
Давайте начнем со Storyboard:
- добавьте на View несколько кнопок — у нас будет два справочника («Заказчики» и «Услуги»), один документ («Заказ») и один отчет по документам
- добавьте Navigation Controller (меню
Editor\Embed In\Navigation Controller
) - добавьте новый Table View Controller с заголовком (Title)
Customers
- соедините добавленный Table View Controller с соответствующей кнопкой основного меню (
Action Segue\Show
)
Теперь необходимо добавить свой класс для Table View Controller:
- меню File \ New \ File…
- в качестве шаблона выбираем Cocoa Class
- выбираем в качестве родительского класса
UITableViewController
и указываем имя нашего класса —CustomersTableViewController
- выбираем где хранить файл и жмем Create
Незабываем указать этот, созданный нами, класс нашему Table View Controller (
Identity Inspector\Custom Class\Class
).Я не буду здесь использовать Prototype Cells и создавать «кастомный» класс для ячеек таблицы (чтобы сосредоточиться на других вещах), поэтому давайте установим количество таких ячеек равным нулю (Attributes Inspector\Table View\Prototype Cells
).
Теперь нам требуется определить источник данных, чтобы реализовать протокол Table View Data Source. В прошлой части мы познакомились с NSFetchRequest и, на первый взгляд, он вроде как подходит для этой цели. С его помощью можно получить список всех объектов в виде массива, что, собственно, нам и нужно. Но мы хотим не только смотреть на список Заказчиков, мы хотим их добавлять, удалять и редактировать. В этом случае, нам придется отслеживать все эти изменения вручную и каждый раз, опять вручную, обновлять наш список. Звучит не очень, да? Но есть другой вариант — NSFetchedResultsController, он очень похож на NSFetchRequest, но он не только возвращает массив нужных нам объектов в момент запроса, но и продолжает следить за всеми записями: если какая-то запись измениться — он нам сообщит об этом, если какие-нибудь записи подгрузятся в фоне через другой управляемый контекст — он нам тоже сообщит об этом. Нам останется только обработать это событие.
Давайте реализуем NSFetchedResultsController в нашем модуле. Я сначала приведу весь код, а следом прокомментирую.
// CustomersTableViewController.swift
// core-data-habrahabr-swift
import UIKit
import CoreData
class CustomersTableViewController: UITableViewController {
var fetchedResultsController:NSFetchedResultsController = {
let fetchRequest = NSFetchRequest(entityName: "Customer")
let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: CoreDataManager.instance.managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)
return fetchedResultsController
}()
override func viewDidLoad() {
super.viewDidLoad()
do {
try fetchedResultsController.performFetch()
} catch {
print(error)
}
}
// MARK: - Table View Data Source
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let sections = fetchedResultsController.sections {
return sections[section].numberOfObjects
} else {
return 0
}
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let customer = fetchedResultsController.objectAtIndexPath(indexPath) as! Customer
let cell = UITableViewCell()
cell.textLabel?.text = customer.name
return cell
}
}
В разделе определения переменных мы создаем объект fetchedResultsController с типом
NSFetchedResultsController
. Как видите, он создается на базе NSFetchRequest
(я создал NSFetchRequest на основании сущности «Customer» и задал сортировку по имени Заказчика). Затем мы создаем сам NSFetchedResultsController
, передав в его конструктор NSFetchRequest
и нужный нам управляемый контекст, дополнительные параметры конструктора (sectionNameKeyPath, cacheName) мы здесь использовать не будем.Затем, при загрузке нашего View Controller (func viewDidLoad()
) мы запускаем fetchedResultsController на выполнение:
try fetchedResultsController.performFetch()
Также нам надо переопределить две функции для реализации Table View Data Source:
- в первой функции мы возвращаем количество объектов в текущей секции (по факту, секции мы здесь не используем, поэтому все объекты будут находиться в одной единственной секции)
- во второй — программно конструируем ячейку для каждого объекта и возвращаем ее.
Давайте проверим! Если сейчас запустить приложение и перейти в нашем меню в
«Customers»
, то мы увидем всех наших заказчиков, которых добавили в прошлой части статьи. Это было не слишком сложно, да? Прежде чем продолжать, давайте кое-что немного оптимизируем — создание объекта NSFetchedResultsController не отличается лаконичностью, а нам его надо будет также создавать и для других наших сущностей. При этом, по сути, меняться будет только имя сущности и, возможно, имя поля сортировки. Чтобы не заниматься «копи-пастой» давайте вынесем создание этого объекта в CoreDataManager.
import CoreData
import Foundation
class CoreDataManager {
// Singleton
static let instance = CoreDataManager()
// Entity for Name
func entityForName(entityName: String) -> NSEntityDescription {
return NSEntityDescription.entityForName(entityName, inManagedObjectContext: self.managedObjectContext)!
}
// Fetched Results Controller for Entity Name
func fetchedResultsController(entityName: String, keyForSort: String) -> NSFetchedResultsController {
let fetchRequest = NSFetchRequest(entityName: entityName)
let sortDescriptor = NSSortDescriptor(key: keyForSort, ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: CoreDataManager.instance.managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)
return fetchedResultsController
}
// MARK: - Core Data stack
// ...
С учетом этого, определение fetchedResultsController измениться на следующее:
var fetchedResultsController = CoreDataManager.instance.fetchedResultsController("Customer", keyForSort: "name")
Теперь нам надо сделать так, чтобы при выборе какого-нибудь Заказчика открывалась «карточка» со всеми его данными, которые, при необходимости, можно было редактировать. Давайте для этого добавим еще один View Controller (зададим ему заголовок
«Customer»
) и соединим его с нашим Table View Controller.В качестве типа переход между контроллерами выберите Present Modally
.
Также нам надо будет обращаться по имени к этому Segue, давайте укажем имя — customersToCustomer
.
Нам понадобиться свой класс для этого View Controller — все аналогично тому, что мы делали для Table View Controller, только в качестве родительского класса выбираем — UIViewController
, имя класса — CustomerViewController
.
И указываем этот класс для нашего нового View Controller.
Теперь добавим Navigation Bar с двумя кнопками (Save — для сохранения изменений и Cancel — для отмены). Также нам необходимы два текстовых поля для отображения и редактирования информации (name и info). Сделаем два Action (для Save и Cancel) и два Outlet (для name и info).
Интерфейс нашей «карточки» Заказчика готов, теперь надо написать немного кода. Логика будет следующая: при переходе в «карточку» Заказчика из списка Заказчиков мы будем передавать объект customer (Заказчик) на основании выбранной строки списка. При открытии «карточки» данные из этого объекта будут загружаться в элементы интерфейса (name
, info), а при сохранении объекта — наоборот, содержимое элементов интерфейса будет переноситься в поля сохраняемого объекта.
Также, нам надо учесть то, что у нас есть обязательное для заполнение поле — name. Если пользователь попробует сохранить Заказчика с пустым именем, то он получит критическую ошибку. Чтобы этого не произошло, давайте добавим проверку корректности сохраняемых данных: если данные не корректные, то будем показывать соответствующую предупреждение и блокировать запись такого объекта. Пользователь должен либо ввести корректные данные, либо отказаться от записи такого объекта.
И последнее, что нам надо здесь учесть: наверняка, нам захочется не только редактировать существующих Заказчиков, но и добавлять новых. Делать это мы будем следующим образом: в списке Заказчиков добавим кнопку для создания нового Заказчика, которая будет открывать нашу «карточку» передавая в нее nil. А при сохранении данных «карточки» Заказчика мы будем проверять, если объект customer у нас еще не создан (то есть это ввод нового Заказчика), то будем его сразу создавать.
Таким образом, у нас получиться примерно следующий код.
// CustomerViewController.swift
// core-data-habrahabr-swift
import UIKit
class CustomerViewController: UIViewController {
var customer: Customer?
@IBAction func cancel(sender: AnyObject) {
dismissViewControllerAnimated(true, completion: nil)
}
@IBAction func save(sender: AnyObject) {
if saveCustomer() {
dismissViewControllerAnimated(true, completion: nil)
}
}
@IBOutlet weak var nameTextField: UITextField!
@IBOutlet weak var infoTextField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
// Reading object
if let customer = customer {
nameTextField.text = customer.name
infoTextField.text = customer.info
}
}
func saveCustomer() -> Bool {
// Validation of required fields
if nameTextField.text!.isEmpty {
let alert = UIAlertController(title: "Validation error", message: "Input the name of the Customer!", preferredStyle: .Alert)
alert.addAction(UIAlertAction(title: "OK", style: .Cancel, handler: nil))
self.presentViewController(alert, animated: true, completion: nil)
return false
}
// Creating object
if customer == nil {
customer = Customer()
}
// Saving object
if let customer = customer {
customer.name = nameTextField.text
customer.info = infoTextField.text
CoreDataManager.instance.saveContext()
}
return true
}
}
Теперь давайте вернемся в Table View Controller и добавим кнопку создания нового Заказчика (
Navigation Item + Bar Button Item
, аналогично карточке Заказчика). И создадим для этой кнопки Action с именем AddCustomer
.Этот Action будет открывать «карточку» для создания нового Заказчика, передавая в нее nil.
@IBAction func AddCustomer(sender: AnyObject) {
performSegueWithIdentifier("customersToCustomer", sender: nil)
}
Осталось сделать так, чтобы при выборе какого-нибудь существующего Заказчика, открывалась его «карточка». Для этого нам понадобиться две процедуры.
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let customer = fetchedResultsController.objectAtIndexPath(indexPath) as? Customer
performSegueWithIdentifier("customersToCustomer", sender: customer)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "customersToCustomer" {
let controller = segue.destinationViewController as! CustomerViewController
controller.customer = sender as? Customer
}
}
В первой процедуре (при выделении строки списка) мы «считываем» текущего Заказчика, а во второй (при переходе из списка в «карточку») — присваиваем ссылку на выбранного Заказчика переменной
customer
нашей «карточки», чтобы при ее открытии мы могли считать все данные объекта.Давайте теперь запустим наше приложение и убедимся, что все работает как надо.
Приложение работает, мы можем вводить новых Заказчиков, редактировать существующих, но информация в списке автоматически не обновляется и у нас нет механизма, чтобы удалять ненужного (или ошибочно введенного) Заказчика. Давайте это исправим.
Так как мы здесь используем NSFetchedResultsController, который «знает» о всех этих изменениях, то нам надо просто его «послушать». Для этого надо реализовать протокол делегата NSFetchedResultsControllerDelegate. Объявим, что мы реализуем этот протокол:
class CustomersTableViewController: UITableViewController, NSFetchedResultsControllerDelegate {
Объявим себя делегатом NSFetchedResultsController:
override func viewDidLoad() {
super.viewDidLoad()
fetchedResultsController.delegate = self
do {
try fetchedResultsController.performFetch()
} catch {
print(error)
}
}
И добавим следующую реализацию этого протокола:
// MARK: - Fetched Results Controller Delegate
func controllerWillChangeContent(controller: NSFetchedResultsController) {
tableView.beginUpdates()
}
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: .Automatic)
}
case .Update:
if let indexPath = indexPath {
let customer = fetchedResultsController.objectAtIndexPath(indexPath) as! Customer
let cell = tableView.cellForRowAtIndexPath(indexPath)
cell!.textLabel?.text = customer.name
}
case .Move:
if let indexPath = indexPath {
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
}
if let newIndexPath = newIndexPath {
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Automatic)
}
case .Delete:
if let indexPath = indexPath {
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
}
}
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
tableView.endUpdates()
}
Несмотря на сравнительно больший объем — она достаточно простая. Здесь мы получаем информацию о том, какой объект и как именно изменился, и, в зависимости от типа изменения, мы выполняем различные действия:
- Insert (добавление) — вставляем новую строку по указанному индексу (строка добавится не просто в конец списка, а в свое место в списке в соответствии с заданной сортировкой)
- Update (обновление) — данные объекта изменились, получаем строку из нашего списка по указанному индексу и обновляем информацию о ней
- Move (перемещение) — порядок строк изменился (например, Заказчика переименовали и он теперь располагается в соответствии с сортировкой в другом месте), удаляем строку оттуда, где она была и добавляем уже по новому индексу
- Delete (удаление) — удаляем строку по указанному индексу.
Также у нас есть две «вспомогательные» функции,
controllerWillChangeContent
и controllerDidChangeContent
, которые, соответственно, информируют о начале и окончании изменения данных. С помощью этих функций мы сообщаем нашему Table View
, что сейчас мы кое-что изменим в тех данных, которые он отображает (это необходимо для его корректной работы).Осталось только реализовать удаление Заказчика. Это делается довольно просто, нам понадобиться переопределить всего одну небольшую процедуру.
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
let managedObject = fetchedResultsController.objectAtIndexPath(indexPath) as! NSManagedObject
CoreDataManager.instance.managedObjectContext.deleteObject(managedObject)
CoreDataManager.instance.saveContext()
}
}
При поступлении команды удаления мы получаем текущий объект по индексу и передаем его управляемому контексту для удаления. Обратите внимание, что тип объекта для удаления должен быть
NSManagedObject
.На этом работа со справочником «Заказчики» завершена. Давайте запустим приложение и проверим его работу.
Как видете, ничего сверхсложного, Core Data прекрасно сочетается со стандартными элементами интерфейса.
// CustomersTableViewController.swift
// core-data-habrahabr-swift
import UIKit
import CoreData
class CustomersTableViewController: UITableViewController, NSFetchedResultsControllerDelegate {
var fetchedResultsController = CoreDataManager.instance.fetchedResultsController("Customer", keyForSort: "name")
override func viewDidLoad() {
super.viewDidLoad()
fetchedResultsController.delegate = self
do {
try fetchedResultsController.performFetch()
} catch {
print(error)
}
}
@IBAction func AddCustomer(sender: AnyObject) {
performSegueWithIdentifier("customersToCustomer", sender: nil)
}
// MARK: - Table View Data Source
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let sections = fetchedResultsController.sections {
return sections[section].numberOfObjects
} else {
return 0
}
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let customer = fetchedResultsController.objectAtIndexPath(indexPath) as! Customer
let cell = UITableViewCell()
cell.textLabel?.text = customer.name
return cell
}
// MARK: - Table View Delegate
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
let managedObject = fetchedResultsController.objectAtIndexPath(indexPath) as! NSManagedObject
CoreDataManager.instance.managedObjectContext.deleteObject(managedObject)
CoreDataManager.instance.saveContext()
}
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let customer = fetchedResultsController.objectAtIndexPath(indexPath) as? Customer
performSegueWithIdentifier("customersToCustomer", sender: customer)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "customersToCustomer" {
let controller = segue.destinationViewController as! CustomerViewController
controller.customer = sender as? Customer
}
}
// MARK: - Fetched Results Controller Delegate
func controllerWillChangeContent(controller: NSFetchedResultsController) {
tableView.beginUpdates()
}
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: .Automatic)
}
case .Update:
if let indexPath = indexPath {
let customer = fetchedResultsController.objectAtIndexPath(indexPath) as! Customer
let cell = tableView.cellForRowAtIndexPath(indexPath)
cell!.textLabel?.text = customer.name
}
case .Move:
if let indexPath = indexPath {
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
}
if let newIndexPath = newIndexPath {
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Automatic)
}
case .Delete:
if let indexPath = indexPath {
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
}
}
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
tableView.endUpdates()
}
}
Справочник «Услуги»
Справочник услуги у нас имеет такую же структуру и логику работы, как и справочник заказчиков. Отличия минимальные, поэтому я не буду здесь все детально расписывать, а просто приведу краткий порядок действий (я уверен, что вы можете легко все сделать самостоятельно по данному конспекту):
- создаем новый Table View Controller и связываем его с кнопкой
«Services»
- создаем и назначаем для него новый класс
ServicesTableViewController
(на основанииUITableViewController
) - импортируем (
import
)CoreData
, добавляемfetchedResultsController
(на основании сущностиService
) и при загрузке контроллера запускаем его на выполнение - добавляем две процедуры для реализации Table View Data Source, первая — возвращает количество строк, вторая возвращает строку с информацией об объекте по указанному индексу
- создаем новый View Controller для отображения «карточки» услуги и располагаем на нем элементы интерфейса (все аналогично «карточке» заказчика)
- создаем и назначаем новый класс
ServiceViewController
(на основанииUIViewController
) для этого контроллера - создаем два Action (кнопки
Save
иCancel
) и два Outlet (поляname
иinfo
) - добавляем необходимый код (объявляем переменную
service
, прописываем процедуры загрузки и сохранения объекта, не забываем о проверке данных перед записью) - добавляем связь между
ServicesTableViewController
иServiceViewController
с именемservicesToService
(Segue \ Present Modally
) - возвращаемся в
ServicesTableViewController
и добавляем кнопку Add для добавления новой услуги (Navigation Item \ Bar Button Item
) и создаем для нее Action с именемAddService
- прописываем необходимый для переход в карточку новой «услуги» код и реализуем методы Table View Delegate (переход в «карточку» выбранной услуги)
- реализуем методы протокола
NSFetchedResultsControllerDelegate
и объявляем текущий класс в качестве делегата - все, проверяем!
// ServicesTableViewController.swift
// core-data-habrahabr-swift
import UIKit
import CoreData
class ServicesTableViewController: UITableViewController, NSFetchedResultsControllerDelegate {
var fetchedResultsController = CoreDataManager.instance.fetchedResultsController("Service", keyForSort: "name")
@IBAction func AddService(sender: AnyObject) {
performSegueWithIdentifier("servicesToService", sender: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
fetchedResultsController.delegate = self
do {
try fetchedResultsController.performFetch()
} catch {
print(error)
}
}
// MARK: - Table View Data Source
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let sections = fetchedResultsController.sections {
return sections[section].numberOfObjects
} else {
return 0
}
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let service = fetchedResultsController.objectAtIndexPath(indexPath) as! Service
let cell = UITableViewCell()
cell.textLabel?.text = service.name
return cell
}
// MARK: - Table View Delegate
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
let managedObject = fetchedResultsController.objectAtIndexPath(indexPath) as! NSManagedObject
CoreDataManager.instance.managedObjectContext.deleteObject(managedObject)
CoreDataManager.instance.saveContext()
}
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let service = fetchedResultsController.objectAtIndexPath(indexPath) as? Service
performSegueWithIdentifier("servicesToService", sender: service)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "servicesToService" {
let controller = segue.destinationViewController as! ServiceViewController
controller.service = sender as? Service
}
}
// MARK: - Fetched Results Controller Delegate
func controllerWillChangeContent(controller: NSFetchedResultsController) {
tableView.beginUpdates()
}
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: .Automatic)
}
case .Update:
if let indexPath = indexPath {
let service = fetchedResultsController.objectAtIndexPath(indexPath) as! Service
let cell = tableView.cellForRowAtIndexPath(indexPath)
cell!.textLabel?.text = service.name
}
case .Move:
if let indexPath = indexPath {
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
}
if let newIndexPath = newIndexPath {
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Automatic)
}
case .Delete:
if let indexPath = indexPath {
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
}
}
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
tableView.endUpdates()
}
}
// ServiceViewController.swift
// core-data-habrahabr-swift
import UIKit
class ServiceViewController: UIViewController {
@IBOutlet weak var nameTextField: UITextField!
@IBOutlet weak var infoTextField: UITextField!
@IBAction func cancel(sender: AnyObject) {
dismissViewControllerAnimated(true, completion: nil)
}
@IBAction func save(sender: AnyObject) {
if saveService() {
dismissViewControllerAnimated(true, completion: nil)
}
}
var service: Service?
override func viewDidLoad() {
super.viewDidLoad()
// Reading object
if let service = service {
nameTextField.text = service.name
infoTextField.text = service.info
}
}
func saveService() -> Bool {
// Validation of required fields
if nameTextField.text!.isEmpty {
let alert = UIAlertController(title: "Validation error", message: "Input the name of the Service!", preferredStyle: .Alert)
alert.addAction(UIAlertAction(title: "OK", style: .Cancel, handler: nil))
self.presentViewController(alert, animated: true, completion: nil)
return false
}
// Creating object
if service == nil {
service = Service()
}
// Saving object
if let service = service {
service.name = nameTextField.text
service.info = infoTextField.text
CoreDataManager.instance.saveContext()
}
return true
}
}
Должно получиться что-то вроде этого:
Документ
С документом будет все немного сложнее, так как каждый документ, во-первых, представлен у нас двумя разными сущностями, а, во-вторых, имеются взаимосвязи, то есть надо обеспечить каким-то образом выбор значения.
Начнем с простого и уже знакомого — создадим Table View Controller со списком документов и View Controller для отображения самого документа (пока без реквизитов, только заготовка). Я не буду повторяться — все по тому же алгоритму, что и справочники.
Создаем два новых контроллера (Table View Controller для списка документов и View Controller для самого документа):
Добавляем Action, создаем fetchedResultsController
и реализуем протоколы:
Делаем заготовку для самого документа:
// OrdersTableViewController.swift
// core-data-habrahabr-swift
import UIKit
import CoreData
class OrdersTableViewController: UITableViewController, NSFetchedResultsControllerDelegate {
var fetchedResultsController = CoreDataManager.instance.fetchedResultsController("Order", keyForSort: "date")
@IBAction func AddOrder(sender: AnyObject) {
performSegueWithIdentifier("ordersToOrder", sender: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
fetchedResultsController.delegate = self
do {
try fetchedResultsController.performFetch()
} catch {
print(error)
}
}
// MARK: - Table View Data Source
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let sections = fetchedResultsController.sections {
return sections[section].numberOfObjects
} else {
return 0
}
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell()
let order = fetchedResultsController.objectAtIndexPath(indexPath) as! Order
configCell(cell, order: order)
return cell
}
func configCell(cell: UITableViewCell, order: Order) {
let formatter = NSDateFormatter()
formatter.dateFormat = "MMM d, yyyy"
let nameOfCustomer = (order.customer == nil) ? "-- Unknown --" : (order.customer!.name!)
cell.textLabel?.text = formatter.stringFromDate(order.date) + "\t" + nameOfCustomer
}
// MARK: - Table View Delegate
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
let managedObject = fetchedResultsController.objectAtIndexPath(indexPath) as! NSManagedObject
CoreDataManager.instance.managedObjectContext.deleteObject(managedObject)
CoreDataManager.instance.saveContext()
}
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let order = fetchedResultsController.objectAtIndexPath(indexPath) as? Order
performSegueWithIdentifier("ordersToOrder", sender: order)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "ordersToOrder" {
let controller = segue.destinationViewController as! OrderViewController
controller.order = sender as? Order
}
}
// MARK: - Fetched Results Controller Delegate
func controllerWillChangeContent(controller: NSFetchedResultsController) {
tableView.beginUpdates()
}
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: .Automatic)
}
case .Update:
if let indexPath = indexPath {
let order = fetchedResultsController.objectAtIndexPath(indexPath) as! Order
let cell = tableView.cellForRowAtIndexPath(indexPath)
configCell(cell!, order: order)
}
case .Move:
if let indexPath = indexPath {
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
}
if let newIndexPath = newIndexPath {
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Automatic)
}
case .Delete:
if let indexPath = indexPath {
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
}
}
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
tableView.endUpdates()
}
}
Несколько замечаний:
- при создании
fetchedResultsController
поле для сортировки мы указываем как »date», то есть документы будут отсортированы по своей дате - для конструирование ячейки используется отдельная вспомогательная функция
configCell
- так как связь между нашим документом и Заказчиком установлена как один-к-одному, то мы можем обращаться к нему сразу «через точку», что мы и делаем при конструировании текста ячейки.
На этом журнал документов у нас будет будет закончен, осталось сделать сам документ. Надо отметить, что все три раздела (два наших справочника и документ) получились очень похожими (с точки зрения реализации) и возникает вопрос о целесообразности использования разных классов и контроллеров для них вместо одного универсального. Такой подход тоже возможен, но похожесть наших контроллеров обусловлена очень простой моделью данных, в реальных приложениях сущности, как правило, все-таки заметно различаются и, как следствие, контроллеры и интерфейсные решения тоже выглядят совершенно по разному.
Переходим к самому интересному — документу. Давайте отразим все необходимые нам элементы интерфейса:
- Дата документа — для этого подойдет Date Picker
- Заказчик — будет представлен двумя элементами: кнопка для выбора Заказчика из списка и поле ввода (недоступное для редактирования) для отображения выбранного элемента
- Признак завершения — воспользуемся Switch
- Признак оплаты — аналогично предыдущему
- Табличная часть — конечно же Table View. Будем выводим информацию по строке табличной части одной строкой текста, не используя «кастомных» ячеек, чтобы не слишком отвлекаться от сути статьи.
Должно получить примерно так (дизайн, конечно, отстой, но это здесь не главное, цель у нас сейчас другая):
Теперь нам надо как-то организовать процесс выбора Заказчика: мы должны открыть список Заказчиков, чтобы пользователь мог выбрать нужного, а затем передать выбранный объект обратно в наш контроллер, чтобы мы могли использовать его в документе. Обычно для этого используется механизм делегирования, то есть создание необходимого протокола и его реализация. Но мы пойдем другим путем — я буду здесь использовать захват контекста с помощью замыкания (подробно рассказывать про сам механизм я не буду, так как есть хорошая статья, посвященная именно этому). Это ненамного сложнее, если вообще сложнее, но быстрее реализуется и выглядит гораздо элегантнее.
Учитывая, что нам в дальнейшем надо будет еще, аналогично Заказчику, выбирать и Услуги, можно было бы создать отдельный универсальный контроллер для выбора значений из списка, но, чтобы сэкономить время, давайте воспользуемся уже готовыми, созданными нами контроллерами (список Заказчиков и список Услуг). Для начала давайте соединим View Controller нашего документа с Table View Controller списка Заказчиков с помощью Segue.
И пропишем вызов этого перехода по кнопке выбора Заказчика.
@IBAction func choiceCustomer(sender: AnyObject) {
performSegueWithIdentifier("orderToCustomers", sender: nil)
}
Также, чтобы реализовать захват контекста, нам надо внести небольшие изменения в наш контроллер, который отвечает за отображение списка контрагентов (CustomersTableViewController.swift
). Во-первых необходимо добавить переменную-замыкание:
// CustomersTableViewController.swift
// core-data-habrahabr-swift
import UIKit
import CoreData
class CustomersTableViewController: UITableViewController, NSFetchedResultsControllerDelegate {
typealias Select = (Customer?) -> ()
var didSelect: Select?
А, во-вторых, изменить процедуру выбора текущей строки списка:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let customer = fetchedResultsController.objectAtIndexPath(indexPath) as? Customer
if let dSelect = self.didSelect {
dSelect(customer)
dismissViewControllerAnimated(true, completion: nil)
} else {
performSegueWithIdentifier("customersToCustomer", sender: customer)
}
}
Обратите внимание на логику: мы используем опциональную переменную-замыкание, если она не определена — то список работает как обычно, в режиме добавления и редактирования данных, если определена — значит список был вызван из документа для выбора Заказчика.
Теперь вернемся обратно в контроллер документа, чтобы реализовать замыкание. Но перед этим определим процедуры загрузки и сохранения документа. Логика работы здесь будет немного отличаться от работы со справочниками. Как мы помним, при создании нового документа у нас передается nil и самого объекта-документа при открытии View еще нет. Если при работе со справочниками нам это не мешало и мы создавали сам объект только перед записью, то для документа мы будем создавать его сразу, так как при редактировании строк табличной части мы должны будем указать ссылку на конкретный документ. В принципе, ничего не мешает использовать такой же подход и для справочников для единообразия, но в целях демонстрации разных подходов оставим оба варианта.
Таким образом, процедура «чтения» данных в элементы формы будет выглядеть следующим образом:
override func viewDidLoad() {
super.viewDidLoad()
// Creating object
if order == nil {
order = Order()
order?.date = NSDate()
}
if let order = order {
dataPicker.date = order.date
switchMade.on = order.made
switchPaid.on = order.paid
textFieldCustomer.text = order.customer?.name
}
}
Обратите внимание: при создании объекта я сразу присвоил документу текущую дату (конструктор
NSDate()
возвращает текущую дату/время). И процедура записи данных: func saveOrder() {
if let order = order {
order.date = dataPicker.date
order.made = switchMade.on
order.paid = switchPaid.on
CoreDataManager.instance.saveContext()
}
}
Теперь давайте, наконец-то, реализуем замыкание для выборка Заказчика, это делается довольно просто:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "orderToCustomers" {
let viewController = segue.destinationViewController as! CustomersTableViewController
viewController.didSelect = { [unowned self] (customer) in
if let customer = customer {
self.order?.customer = customer
self.textFieldCustomer.text = customer.name!
}
}
}
}
При переходе на Table View Controller мы определяем обработчик, согласно которому, при выборе Заказчика, мы присваиваем его нашему объекту-документу, а также отображаем имя Заказчика на соответствующем элементе управления документа.
На этом механизм выбор Заказчика закончен, давайте удостоверимся, что все работает, как надо.
Теперь давайте займемся табличной частью. Здесь уже должно быть все знакомо. Очевидно, что надо создать fetchedResultsController
и реализовать протоколы NSFetchedResultsControllerDelegate