Core Data + Swift для самых маленьких: необходимый минимум (часть 3)

Это заключительная часть статьи о Core Data, предыдущие части доступны здесь: часть 1 и часть 2.

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

f5591dffdd404ca8bffd6c41a9b00924.png

Теперь необходимо добавить свой класс для Table View Controller:

  • меню File \ New \ File…
  • в качестве шаблона выбираем Cocoa Class
    12fc94b68773464d92e6ab41fb1b8a8d.png
  • выбираем в качестве родительского класса UITableViewController и указываем имя нашего класса — CustomersTableViewController2cb3f831596d4da5845aff680359430f.png
  • выбираем где хранить файл и жмем Create

Незабываем указать этот, созданный нами, класс нашему Table View Controller (Identity Inspector\Custom Class\Class).
d9b30e97453b4d12865745a76360a157.png

Я не буду здесь использовать Prototype Cells и создавать «кастомный» класс для ячеек таблицы (чтобы сосредоточиться на других вещах), поэтому давайте установим количество таких ячеек равным нулю (Attributes Inspector\Table View\Prototype Cells).
0c78b14b058941c1980fa69aba0ddefe.png

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

a25c2401d39e485ab20613a6a5931276.pngaa35235ea0f74a4fa1b9489f3935cf5a.png

Прежде чем продолжать, давайте кое-что немного оптимизируем — создание объекта 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.

87cefadbe9cf47fe997d8cb1eb00a7b3.png

В качестве типа переход между контроллерами выберите Present Modally.

52d8e84eadae44f9a7c6ad5d04d2d8b4.png

Также нам надо будет обращаться по имени к этому Segue, давайте укажем имя — customersToCustomer.

c1a18799a6e249af95e26901361e7dcf.png

Нам понадобиться свой класс для этого View Controller — все аналогично тому, что мы делали для Table View Controller, только в качестве родительского класса выбираем — UIViewController, имя класса — CustomerViewController.

4fead33a58b14fe5bef3dfdaf135e350.png

И указываем этот класс для нашего нового View Controller.

62aeba77773d4aa09e550aa36debd6c8.png

Теперь добавим Navigation Bar с двумя кнопками (Save — для сохранения изменений и Cancel — для отмены). Также нам необходимы два текстовых поля для отображения и редактирования информации (name и info). Сделаем два Action (для Save и Cancel) и два Outlet (для name и info).

90fada1cc91c4f91b21af13031557c4d.png

Интерфейс нашей «карточки» Заказчика готов, теперь надо написать немного кода. Логика будет следующая: при переходе в «карточку» Заказчика из списка Заказчиков мы будем передавать объект 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.

8d5a22a4e5224d59b6479f333b8639e5.png

Этот 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 нашей «карточки», чтобы при ее открытии мы могли считать все данные объекта.

Давайте теперь запустим наше приложение и убедимся, что все работает как надо.

eac86e2492f44703b77cf22a479a8d40.png

Приложение работает, мы можем вводить новых Заказчиков, редактировать существующих, но информация в списке автоматически не обновляется и у нас нет механизма, чтобы удалять ненужного (или ошибочно введенного) Заказчика. Давайте это исправим.

Так как мы здесь используем 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.

На этом работа со справочником «Заказчики» завершена. Давайте запустим приложение и проверим его работу.

962568b38981404db5e9122744c9ff33.png8351ca1e93b04453a415617ca42aa7a2.png

Как видете, ничего сверхсложного, Core Data прекрасно сочетается со стандартными элементами интерфейса.

Текст модуля CustomersTableViewController.swift
//  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
//  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
//  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
    }
}


Xcode
4b634968531b403a841b6bf61e0f7348.png

b46d3d930aaa44c9aa3bb9575b2359a0.png

e2d1857791964e02b00794a291aace41.png


Должно получиться что-то вроде этого:

c9c132bc145042ab9433d06dc872dbb7.pngb6ba8f1b84574429b36c3061a49e121e.png

Документ


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

Начнем с простого и уже знакомого — создадим Table View Controller со списком документов и View Controller для отображения самого документа (пока без реквизитов, только заготовка). Я не буду повторяться — все по тому же алгоритму, что и справочники.

Создаем два новых контроллера (Table View Controller для списка документов и View Controller для самого документа):

bc7005512c7d4028ba606f02dfcf7cbe.png

Добавляем Action, создаем fetchedResultsController и реализуем протоколы:

3291ed2e9691450fb097235164538552.png

Делаем заготовку для самого документа:

30edafdf7f2641319c700b470a66bd9b.png

Текст модуля OrdersTableViewController.swift
//  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. Будем выводим информацию по строке табличной части одной строкой текста, не используя «кастомных» ячеек, чтобы не слишком отвлекаться от сути статьи.

Должно получить примерно так (дизайн, конечно, отстой, но это здесь не главное, цель у нас сейчас другая):

30e2542fe4e14f759ea425fa1056c461.png

Теперь нам надо как-то организовать процесс выбора Заказчика: мы должны открыть список Заказчиков, чтобы пользователь мог выбрать нужного, а затем передать выбранный объект обратно в наш контроллер, чтобы мы могли использовать его в документе. Обычно для этого используется механизм делегирования, то есть создание необходимого протокола и его реализация. Но мы пойдем другим путем — я буду здесь использовать захват контекста с помощью замыкания (подробно рассказывать про сам механизм я не буду, так как есть хорошая статья, посвященная именно этому). Это ненамного сложнее, если вообще сложнее, но быстрее реализуется и выглядит гораздо элегантнее.

Учитывая, что нам в дальнейшем надо будет еще, аналогично Заказчику, выбирать и Услуги, можно было бы создать отдельный универсальный контроллер для выбора значений из списка, но, чтобы сэкономить время, давайте воспользуемся уже готовыми, созданными нами контроллерами (список Заказчиков и список Услуг). Для начала давайте соединим View Controller нашего документа с Table View Controller списка Заказчиков с помощью Segue.

2d10d3b3f43b433bb358fd537f0aa282.png

И пропишем вызов этого перехода по кнопке выбора Заказчика.

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

На этом механизм выбор Заказчика закончен, давайте удостоверимся, что все работает, как надо.

f55e7012ccf7466e8d4bed5bd402059c.png0a8981d4b392473eb02c1eba5f52a681.pngb41c1c8c50934e05b256670f34aa14d1.png

Теперь давайте займемся табличной частью. Здесь уже должно быть все знакомо. Очевидно, что надо создать fetchedResultsController и реализовать протоколы NSFetchedResultsControllerDelegate© Habrahabr.ru