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

в 16:27, , рубрики: core data, ios development, swift, разработка под iOS

Это заключительная часть статьи о 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 (меню EditorEmbed InNavigation Controller)
  • добавьте новый Table View Controller с заголовком (Title) Customers
  • соедините добавленный Table View Controller с соответствующей кнопкой основного меню (Action SegueShow)

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

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

  • меню File New File…
  • в качестве шаблона выбираем Cocoa Class
    Core Data + Swift для самых маленьких: необходимый минимум (часть 3) - 2

  • выбираем в качестве родительского класса UITableViewController и указываем имя нашего класса — CustomersTableViewControllerCore Data + Swift для самых маленьких: необходимый минимум (часть 3) - 3
  • выбираем где хранить файл и жмем Create

Незабываем указать этот, созданный нами, класс нашему Table View Controller (Identity InspectorCustom ClassClass).
Core Data + Swift для самых маленьких: необходимый минимум (часть 3) - 4

Я не буду здесь использовать Prototype Cells и создавать «кастомный» класс для ячеек таблицы (чтобы сосредоточиться на других вещах), поэтому давайте установим количество таких ячеек равным нулю (Attributes InspectorTable ViewPrototype Cells).
Core Data + Swift для самых маленьких: необходимый минимум (часть 3) - 5

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Так как мы здесь используем 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 + Swift для самых маленьких: необходимый минимум (часть 3) - 16 Core Data + Swift для самых маленьких: необходимый минимум (часть 3) - 17

Как видете, ничего сверхсложного, 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

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

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

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

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

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

Документ

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Но, минуточку, если мы будем использовать fetchedResultsController, созданный аналогично предыдущим — мы действительно получим все строки табличной части, но это будут строки всех документов, а нам нужны строки только текущего документа, того с которым работает пользователь.

Для этого нам надо добавить соответствующий фильтр в fetchRequest. Делается это через механизм предикатов (NSPredicate). Мы будем говорить о нем чуть больше в конце статьи, а пока давайте просто добавим для нашего документа (Order.swift) функцию класса, которая будет возвращать табличную часть документа в виде NSFetchedResultsController.

   class func getRowsOfOrder(order: Order) -> NSFetchedResultsController {
        
        let fetchRequest = NSFetchRequest(entityName: "RowOfOrder")
        
        let sortDescriptor = NSSortDescriptor(key: "service.name", ascending: true)
        fetchRequest.sortDescriptors = [sortDescriptor]
        
        let predicate = NSPredicate(format: "%K == %@", "order", order)
        fetchRequest.predicate = predicate
        
        let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: CoreDataManager.instance.managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)
        
        return fetchedResultsController
    }

Обратите внимание на эту строку кода:

let sortDescriptor = NSSortDescriptor(key: "service.name", ascending: true)

Здесь мы задаем в качестве ключа сортировки вложенное поле объекта («через точку»). Разве не замечательная возможность?

Теперь вернемся в OrderViewController.swift, нам надо объявить переменную, которая будет содержать табличную часть и инициализировать ее после инициализации самого документа при загрузке View Controller.

//  OrderViewController.swift
//  core-data-habrahabr-swift

import UIKit
import CoreData

class OrderViewController: UIViewController {

    var order: Order?
    var table: NSFetchedResultsController?

    //… 
 
    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
            table = Order.getRowsOfOrder(order)
            table!.delegate = self
            do {
                try table!.performFetch()
            } catch {
                print(error)
            }
      }
    }

Сразу создадим новый View Controller для отображения данных строки документа и назначим ему новый класс RowOfOrderViewController. Добавим необходимые элементы навигации и управления, Outlet и Action, реализуем процедуры чтения и записи объекта. Также для поля ввода суммы установим цифровую клавиатуру (Keyboard Type = Number Pad).

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

Теперь давайте добавим Segue с именем orderToRowOfOrder (соединив документ и только что созданный View Controller) и реализуем в нашем документе делегаты необходимых протоколов. Все тоже самое, что и в предыдущих контроллерах, здесь ничего принципиального нового нет (чуть ниже я приведу полный текст модуля).

Также, давайте добавим кнопку для добавления строк в табличную часть документа. Здесь есть один нюанс: если раньше при создании нового объекта передавали nil, и сам объект создавали в другом контроллере, то в случае строки табличной части нам каким-то образом надо «прописать» в ней конкретный документ. Это можно сделать разными способами в зависимости от логики работы программы. Мы сделаем наиболее очевидный — передадим не nil, а объект (RowOfOrder), который тут же создадим и установим в нем ссылку на наш документ.

  @IBAction func AddRowOfOrder(sender: AnyObject) {
        if let order = order {
            let newRowOfOrder = RowOfOrder()
            newRowOfOrder.order = order
            performSegueWithIdentifier("orderToRowOfOrder", sender: newRowOfOrder)
        }
    }

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

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

Текст модуля OrderViewController.swift

//  OrderViewController.swift
//  core-data-habrahabr-swift

import UIKit
import CoreData

class OrderViewController: UIViewController, NSFetchedResultsControllerDelegate, UITableViewDataSource, UITableViewDelegate {

    var order: Order?
    var table: NSFetchedResultsController?
    
    @IBOutlet weak var dataPicker: UIDatePicker!
    @IBOutlet weak var textFieldCustomer: UITextField!
    @IBOutlet weak var tableView: UITableView!

    @IBAction func save(sender: AnyObject) {
        saveOrder()
        dismissViewControllerAnimated(true, completion: nil)
   }
    
    @IBAction func cancel(sender: AnyObject) {
        dismissViewControllerAnimated(true, completion: nil)
    }
    
    @IBAction func choiceCustomer(sender: AnyObject) {
        performSegueWithIdentifier("orderToCustomers", sender: nil)
    }
    
    @IBAction func AddRowOfOrder(sender: AnyObject) {
        if let order = order {
            let newRowOfOrder = RowOfOrder()
            newRowOfOrder.order = order
            performSegueWithIdentifier("orderToRowOfOrder", sender: newRowOfOrder)
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.dataSource = self
        tableView.delegate = self
        
        // 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
            table = Order.getRowsOfOrder(order)
            table!.delegate = self
            do {
                try table!.performFetch()
            } catch {
                print(error)
            }
        }
    }

    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?) {
        switch segue.identifier! {
        case "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!
                }
            }
        case "orderToRowOfOrder":
            let controller = segue.destinationViewController as! RowOfOrderViewController
            controller.rowOfOrder = sender as? RowOfOrder
        default:
            break
        }
    }
    
    // MARK: - Table View Data Source
    
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if let sections = table?.sections {
            return sections[section].numberOfObjects
        } else {
            return 0
        }
    }
    
    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let rowOfOrder = table?.objectAtIndexPath(indexPath) as! RowOfOrder
        let cell = UITableViewCell()
        let nameOfService = (rowOfOrder.service == nil) ? "-- Unknown --" : (rowOfOrder.service!.name!)
        cell.textLabel?.text = nameOfService + " - " + String(rowOfOrder.sum)
        return cell
    }
    
    // MARK: - Table View Delegate
    
    func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
        if editingStyle == .Delete {
            let managedObject = table?.objectAtIndexPath(indexPath) as! NSManagedObject
            CoreDataManager.instance.managedObjectContext.deleteObject(managedObject)
            CoreDataManager.instance.saveContext()
        }
    }
    
    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        let rowOfOrder = table?.objectAtIndexPath(indexPath) as! RowOfOrder
        performSegueWithIdentifier("orderToRowOfOrder", sender: rowOfOrder)
    }
    
    // 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 rowOfOrder = table?.objectAtIndexPath(indexPath) as! RowOfOrder
                let cell = tableView.cellForRowAtIndexPath(indexPath)!
                let nameOfService = (rowOfOrder.service == nil) ? "-- Unknown --" : (rowOfOrder.service!.name!)
                cell.textLabel?.text = nameOfService + " - " + String(rowOfOrder.sum)
            }
        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()
    }    
}

На этом работа непосредственно с самим документом у нас завершена. Осталось закончить с View Controller, который отображает информацию по строке документа. Здесь мы будем использовать точно такую же логику, как и при работе шапкой документа. Выбор Услуги также сделаем через захват контекста замыканием.

Давайте для начала добавим Segue с именем rowOfOrderToServices, который соединит View Controller строки документа и Table View Controller со списком Услуг. Нам надо немного доработать Table View Controller, чтобы мы могли использовать замыкание. Во-первых, добавим переменную-замыкание:

//  ServicesTableViewController.swift
//  core-data-habrahabr-swift

import UIKit
import CoreData

class ServicesTableViewController: UITableViewController, NSFetchedResultsControllerDelegate {
    typealias Select = (Service?) -> ()
    var didSelect: Select?
    // …

И, во-вторых, изменим функцию выбора строки списка:

   override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        let service = fetchedResultsController.objectAtIndexPath(indexPath) as? Service
        if let dSelect = self.didSelect {
            dSelect(service)
            dismissViewControllerAnimated(true, completion: nil)
        } else {
            performSegueWithIdentifier("servicesToService", sender: service)
        }
    }

Вернемся обратно в RowOfOrderViewController и реализуем замыкание. Здесь все по тому же принципу, что и при выборе Заказчика.

    @IBAction func choiceService(sender: AnyObject) {
        performSegueWithIdentifier("rowOfOrderToServices", sender: nil)
    }
    
    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        if segue.identifier == "rowOfOrderToServices" {
            let controller = segue.destinationViewController as! ServicesTableViewController
            controller.didSelect = {[unowned self] (service) in
                if let service = service {
                    self.rowOfOrder!.service = service
                    self.textFieldService.text = service.name
                }
            }
        }
    }
Текст модуля RowOfOrderViewController.swift

//  RowOfOrderViewController.swift
//  core-data-habrahabr-swift

import UIKit

class RowOfOrderViewController: UIViewController {

    var rowOfOrder: RowOfOrder?
    
    @IBAction func cancel(sender: AnyObject) {
        dismissViewControllerAnimated(true, completion: nil)
    }
    
    @IBAction func save(sender: AnyObject) {
        saveRow()
        dismissViewControllerAnimated(true, completion: nil)
    }
    
    @IBAction func choiceService(sender: AnyObject) {
        performSegueWithIdentifier("rowOfOrderToServices", sender: nil)
    }
    
    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        if segue.identifier == "rowOfOrderToServices" {
            let controller = segue.destinationViewController as! ServicesTableViewController
            controller.didSelect = {[unowned self] (service) in
                if let service = service {
                    self.rowOfOrder!.service = service
                    self.textFieldService.text = service.name
                }
            }
        }
    }
    
    @IBOutlet weak var textFieldService: UITextField!
    @IBOutlet weak var textFieldSum: UITextField!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        if let rowOfOrder = rowOfOrder {
            textFieldService.text = rowOfOrder.service?.name
            textFieldSum.text = String(rowOfOrder.sum)
        } else {
            rowOfOrder = RowOfOrder()
        }
    }
    
    func saveRow() {
        if let rowOfOrder = rowOfOrder {
            rowOfOrder.sum = Float(textFieldSum.text!)!
            CoreDataManager.instance.saveContext()
        }
    }
}

Собственно, всё! На этом работа с документом завершена, давайте все проверим.

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

Важное замечание!
Мы здесь никак не обрабатывали нажатия на кнопку Cancel, что привело к следующей ситуации. Если мы создали новый документ, а потом передумали его сохранять и нажали Cancel, то он останется висеть в качестве «черновика» в нашем журнале документов, так как из текущего контекста Core Data его никто не удалил. К нему можно вернуться и продолжить заполнять, либо можно удалить принудительно. Но если вернуться в основное меню, а потом снова открыть журнал документов, то черновиков уже не будет, так как при открытии журнала мы считывает данные из хранилища. Все то же самое касается и строк документа. Для нашей программы такое поведение кажется логичным, ну, по крайней мере — допустимым. Но, возможно, такое поведение — это совсем не то, что хочется вам в вашей программе. В этом случае, вы должны реализовать свою логику реагирования на такие события. Не забывайте, что в любом случае, поведение программы должно быть для пользователя абсолютно понятным и прозрачным.

Отчет по документам

Этот раздел будет совсем не большим (по сравнению с предыдущим). Мы уже успели немного познакомиться с NSFetchRequest, сейчас рассмотрим его поближе. Давайте сразу создадим новый Table View Controller, создадим и назначим ему новый класс (ReportTableViewController на основании UITableViewController).

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

Мы рассмотрим работу с NSFetchRequest на примере простого отчета, который будет выводить отсортированный по дате список выполненных, но не оплаченных документов. Для этого мы будем использовать два мощных инструмента, которыми обладает NSFetchRequest:

  • NSSortDescriptor — для сортировки данных
  • NSPredicate — для задания различных условий отбора (фильтр).

Начнем с сортировки данных, посмотрите на следующее определение:

var fetchRequest = NSFetchRequest(entityName: "Order")
        
        // Sort Descriptor
        let sortDescriptor = NSSortDescriptor(key: "date", ascending: true)
        fetchRequest.sortDescriptors = [sortDescriptor]

        return fetchRequest
    }()

Здесь мы создаем новый объект сортировки данных (NSSortDescriptor), передавая его конструктору строку, содержащую имя поля сортировки, и указываем желаемое направление сортировки (ascending: true — по возрастанию, false — по убыванию). Обратите внимание, что объекту NSFetchRequest мы передаем объект сортировки в виде массива. Что это значит? Да, именно это, — мы можем передавать несколько правил сортировки одновременно в виде массива.

Также напомню, что в качество поля сортировки можно указывать составные поля «через точку» (мы это делали когда сортировали строки табличной части документа). Давайте добавим второй объект сортировки для того, чтобы внутри даты отсортировать документы по наименованию Заказчика.

   var fetchRequest:NSFetchRequest = {
        var fetchRequest = NSFetchRequest(entityName: "Order")
        
        // Sort Descriptor
        let sortDescriptor1 = NSSortDescriptor(key: "date", ascending: true)
        let sortDescriptor2 = NSSortDescriptor(key: "customer.name", ascending: true)
        fetchRequest.sortDescriptors = [sortDescriptor1, sortDescriptor2]
      
        return fetchRequest
    }()

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

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

        // Predicate
        let predicate = NSPredicate(format: "%K == %@", "made", true)
        fetchRequest.predicate = predicate

В конструктор передается форматная строка, следом передаются аргументы. В зависимости от форматной строки, количество передаваемых параметров может меняться. Давайте подробнее рассмотрим форматную строку — здесь используется что-то вроде своего собственного языка запросов. "%K" — означает имя поля (свойства) объекта, "%@" — значение этого поля. Следом указываются аргументы (фактические значения, которые надо подставить в отбор), строго в том же порядке следования. То есть эта форматная строка означает следующее: Order.made == true.

Можно использовать не только операцию ==, но <, >=, != и так далее. Также можно использовать ключевые слова, такие как CONTAINS, LIKE, MATCHES, BEGINSWITH, ENDSWITH, а также AND и OR. Еще можно использовать регулярные выражения. Это действительно очень мощный инструмент. Я не буду здесь перечислять все возможные варианты, они хорошо представлены в официальной документации Apple. В качестве аргументов для имени поля можно, как и в NSSortDescriptor, использовать составные поля («через точку»). Но нельзя использовать несколько предикатов одновременно, вместо этого следует использовать более сложное условие в единственном предикате. С учетом этого, итоговое определение предиката в нашем отчете будет следующее:

    var fetchRequest:NSFetchRequest = {
        var fetchRequest = NSFetchRequest(entityName: "Order")
        
        // Sort Descriptor
        let sortDescriptor1 = NSSortDescriptor(key: "date", ascending: true)
        let sortDescriptor2 = NSSortDescriptor(key: "customer.name", ascending: true)
        fetchRequest.sortDescriptors = [sortDescriptor1, sortDescriptor2]

        // Predicate
        let predicate = NSPredicate(format: "%K == %@ AND %K == %@", "made", true, "paid", false)
        fetchRequest.predicate = predicate
        
        return fetchRequest
    }()

Осталось только реализовать протокол UITableViewDataSource (это вы уже умеете, здесь ничего нового) и можно проверять.

Текст модуля ReportTableViewController.swift

//  ReportTableViewController.swift
//  core-data-habrahabr-swift

import UIKit
import CoreData

class ReportTableViewController: UITableViewController {

    var fetchRequest:NSFetchRequest = {
        var fetchRequest = NSFetchRequest(entityName: "Order")
        
        // Sort Descriptor
        let sortDescriptor1 = NSSortDescriptor(key: "date", ascending: true)
        let sortDescriptor2 = NSSortDescriptor(key: "customer.name", ascending: true)
        fetchRequest.sortDescriptors = [sortDescriptor1, sortDescriptor2]

        // Predicate
        let predicate = NSPredicate(format: "%K == %@ AND %K == %@", "made", true, "paid", false)
        fetchRequest.predicate = predicate
        
        return fetchRequest
    }()
    
    var report: [Order]?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        do {
            report = try CoreDataManager.instance.managedObjectContext.executeFetchRequest(fetchRequest) as? [Order]
        } catch {
            print(error)
        }
    }

    // MARK: - Table View Data Source

    override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if let report = report {
            return report.count
        } else {
            return 0
        }
    }

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        
        let cell = UITableViewCell()
        
        if let report = report {
            let order = report[indexPath.row]
            
            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
        }
        
        return cell
    }
}

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

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

Итоговый вид Storyboard

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

Заключение

На примере простого приложения мы рассмотрели все основные моменты работы с Core Data и получили, за достаточно короткий срок, полностью работоспособное приложение. Дизайн, конечно, как минимум просит доработки, но данная публикация преследовала другую цель. Стоит еще раз отметить, что вся непосредственная работа с данными, в том числе организация хранилища данных и все возможные проверки на согласованность, скрыта «под капотом» Core Data, мы практически не задумывались об этом, а работали с управляемыми объектам, как с обычными объектами ООП.

Я надеюсь, что смог достаточно понятно объяснить основные приемы работы с Core Data, которые, по моему мнению, необходимы любому iOS-разработчику. Замечательно, если вы перестали бояться и, хотя бы немножко, полюбили Core Data. Спасибо за внимание.

Этот проект на GitHub

Автор: angryscorp

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js