📜 ⬆️ ⬇️

Core Data + Swift for the smallest: minimum necessary (part 3)

This is the final part of the article about Core Data , the previous parts are available here: Part 1 and Part 2 .

In this article, we will turn to face the user and work on the interface part; NSFetchRequest and NSFetchedResultsController will help us in this. This part was quite large, but I see no reason to split it up into several publications. More accurately, under the cut a lot of code and pictures.

The interface is an ambiguous thing and, depending on the requirements for the product, can significantly change. In this article I will not give him too much time, more precisely, I will devote very little (I mean following the Guidelines and the like). My task in this part of the article is to show how Core Data can fit organically into the iOS controls. Therefore, I will use for this purpose such an interface, using which the interaction of controls and Core Data will look simpler and clearer. It is obvious that in the real application of the interface part it will be necessary to devote much more time.

Directories


Before we begin, let's AppDelegate.swift initial view to the application delegate module ( AppDelegate.swift ), in which we experimented in the last part of the article.
')
 // 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() } } 

Let's start with Storyboard:




Now you need to add your class to the Table View Controller :

We are not forgetting to indicate this class created by us to our Table View Controller ( Identity Inspector\Custom Class\Class ).


I will not use Prototype Cells here and create a “custom” class for table cells (to focus on other things), so let's set the number of such cells to zero ( Attributes Inspector\Table View\Prototype Cells ).


Now we need to define a data source in order to implement the Table View Data Source protocol. In the last part we met with NSFetchRequest and, at first glance, it seems to be suitable for this purpose. With it, you can get a list of all objects in the form of an array, which, in fact, we need. But we want not only to look at the list of customers, we want to add, delete and edit them. In this case, we will have to track all these changes manually and each time, again manually, update our list. It doesn't sound very much, does it? But there is another option - NSFetchedResultsController , it is very similar to NSFetchRequest , but it not only returns an array of objects we need at the time of the request, but continues to monitor all records: if any record changes, it will tell us about it, if any. Some records will be loaded in the background through another managed context - it will also inform us about this. We will only have to handle this event.

Let's implement the NSFetchedResultsController in our module. I will first give all the code, and then comment on it.

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

In the variable definition section, we create a fetchedResultsController object of type NSFetchedResultsController . As you can see, it is created based on NSFetchRequest (I created NSFetchRequest based on the “Customer” entity and set the sort by Customer name). Then we create the NSFetchedResultsController itself, passing in its NSFetchRequest constructor and the managed context we need, we will not use additional constructor parameters (sectionNameKeyPath, cacheName) here.

Then, when loading our View Controller ( func viewDidLoad() ), we run fetchedResultsController :
  try fetchedResultsController.performFetch() 

We also need to override two functions to implement the Table View Data Source :

Let's check! If we start the application now and go to our «Customers» menu, we will see all of our customers who were added in the last part of the article. It wasn't too hard, was it?



Before proceeding, let's optimize something a bit - creating an NSFetchedResultsController object is not concise, and we will also need to create it for our other entities. In this case, in essence, only the name of the entity and, possibly, the name of the sort field will change. In order not to engage in "copy-paste", let's move out the creation of this object in the 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 // ... 

In view of this, the definition of fetchedResultsController changes to the following:

  var fetchedResultsController = CoreDataManager.instance.fetchedResultsController("Customer", keyForSort: "name") 

Now we need to make sure that when choosing a Customer, a “card” is opened with all its data, which, if necessary, can be edited. Let's add another View Controller for this (let’s give it the title «Customer» ) and connect it to our Table View Controller .



For the type of transition between controllers, select Present Modally .



We will also need to address by name this Segue , let's specify the name - customersToCustomer .



We need our own class for this View Controller - everything is similar to what we did for the Table View Controller , only as the parent class we select - UIViewController , the class name is CustomerViewController .



And we specify this class for our new View Controller .



Now add a Navigation Bar with two buttons ( Save - to save changes and Cancel - to cancel). We also need two text fields for displaying and editing information ( name and info ). Make two Actions (for Save and Cancel) and two Outlets (for name and info).



The interface of our "card" of the Customer is ready, now we need to write some code. The logic will be as follows: when switching to the “card” of the Customer from the list of Customers, we will transfer the customer object (Customer) based on the selected row of the list. When opening a “card”, data from this object will be loaded into interface elements ( name , info ), and when the object is saved, on the contrary, the contents of the interface elements will be transferred to the fields of the saved object.

Also, we need to take into account the fact that we have a required field - name . If the user tries to save the Customer with an empty name, he will receive a critical error. To prevent this, let's add validation of the stored data: if the data is not correct, then we will show the corresponding warning and block the recording of such an object. The user must either enter the correct data or refuse to record such an object.

And the last thing we need to take into account here: for sure, we want to not only edit existing Customers, but also add new ones. We will do this as follows: in the list of Customers we will add a button to create a new Customer, which will open our "card" by transferring nil to it. And when saving the data of the “card” of the Customer, we will check if the customer object has not yet been created (that is, to enter a new Customer), then we will immediately create it.

Thus, we get about the following code.

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

Now let's go back to the Table View Controller and add a button to create a new customer ( Navigation Item + Bar Button Item , similar to the Customer card). And create an Action for this button named AddCustomer .



This Action will open a “card” to create a new Customer, transferring nil to it.

  @IBAction func AddCustomer(sender: AnyObject) { performSegueWithIdentifier("customersToCustomer", sender: nil) } 

It remains to make sure that when choosing an existing Customer, its “card” opens. For this we need two procedures.

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

In the first procedure (when selecting the list line) we “read” the current Customer, and in the second (when moving from the list to the “card”), we assign a reference to the selected customer our “card” customer variable so that when opening it we can read all object data.

Let's now launch our application and make sure that everything works as it should.



The application works, we can enter new customers, edit existing ones, but the information in the list is not automatically updated and we do not have a mechanism to delete an unnecessary (or mistakenly entered) Customer. Let's fix it.

Since we are using NSFetchedResultsController , which “knows” about all these changes, we just need to “listen” to it. To do this, you must implement the delegate protocol NSFetchedResultsControllerDelegate . Let's announce that we implement this protocol:

 class CustomersTableViewController: UITableViewController, NSFetchedResultsControllerDelegate { 

Declare yourself a delegate to NSFetchedResultsController :

  override func viewDidLoad() { super.viewDidLoad() fetchedResultsController.delegate = self do { try fetchedResultsController.performFetch() } catch { print(error) } } 

And add the following implementation of this protocol:

  // 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() } 

Despite the relatively large volume - it is quite simple. Here we get information about which object and how it has changed, and, depending on the type of change, we perform various actions:

We also have two “helper” functions, controllerWillChangeContent and controllerDidChangeContent , which, respectively, inform about the beginning and end of the data change. Using these functions, we inform our Table View that now we will change something in the data that it displays (this is necessary for its correct operation).

It remains only to implement the removal of the customer. This is done quite simply, we need to override just one small procedure.

 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() } } 

When a delete command is received, we retrieve the current object by index and pass it to the managed context for deletion. Note that the type of the object to be deleted must be NSManagedObject .

This completes the work with the reference book "Customers". Let's run the application and check its work.



As you see, nothing supercomplex, Core Data combines perfectly with standard interface elements.

The text of the module 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() } } 



Directory "Services"


The directory of services we have has the same structure and logic of work as the directory of customers. The differences are minimal, so I will not describe everything in detail here, but simply give a brief procedure (I'm sure that you can easily do everything on your own according to this summary):


ServicesTableViewController.swift module text
 // 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() } } 


Text module 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






It should get something like this:



Document


Everything will be a little more complicated with the document, since each document, firstly, is represented by two different entities, and, secondly, there are interrelations, that is, it is necessary to ensure the choice of meaning in some way.

Let's start with a simple and familiar one - let's create a Table View Controller with a list of documents and a View Controller to display the document itself (for now, no details, only a blank). I will not repeat - all the same algorithm as the reference books.

Create two new controllers ( Table View Controller for the document list and View Controller for the document itself):



Add an Action , create fetchedResultsControllerand implement protocols:



Making a blank for the document itself:



The text of the OrdersTableViewController.swift module
 // 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() } } 


A few notes:

At this, the document log will be finished, it remains to make the document itself. It should be noted that all three sections (two of our reference books and a document) turned out to be very similar (from the point of view of implementation) and the question arises whether it is advisable to use different classes and controllers for them instead of one universal one. This approach is also possible, but the similarity of our controllers is due to a very simple data model, in real-life applications, entities, as a rule, still noticeably differ and, as a result, controllers and interface solutions also look completely different.

We turn to the most interesting - the document. Let's reflect all the interface elements we need:

Should get something like this (design, of course, it sucks, but this is not the main goal we have now different):



Now we need to somehow organize the Client selection process: we have to open a list of customers, so the user can choose the right and then pass selected object back to our controller so we can use it in the document. Usually this is done using the delegation mechanism, that is, the creation of the necessary protocol and its implementation. But we will go the other way - I will use the context capture with the help of a closure here (I will not talk in detail about the mechanism itself, since there is a good article dedicated to just this ). It is not much more difficult, if at all more difficult, but it is faster implemented and looks much more elegant.

Considering that in the future we will need to choose Services as well, similarly to the Customer, it would be possible to create a separate universal controller to select values ​​from the list, but to save time, let's use the ready-made controllers we created (list of Customers and Services list ). To get started, let's connect the View Controller of our document with the Table View Controller of the list of Customers using Segue .



And we will register a call of this transition on the button of the choice of the Customer.

  @IBAction func choiceCustomer(sender: AnyObject) { performSegueWithIdentifier("orderToCustomers", sender: nil) } 


Also, in order to implement context capture, we need to make small changes to our controller, which is responsible for displaying the list of counterparties ( CustomersTableViewController.swift). First you need to add a closure variable:

 // CustomersTableViewController.swift // core-data-habrahabr-swift import UIKit import CoreData class CustomersTableViewController: UITableViewController, NSFetchedResultsControllerDelegate { typealias Select = (Customer?) -> () var didSelect: Select? 

And, secondly, change the procedure for selecting the current line of the list:

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

Pay attention to the logic: we use an optional closure variable, if it is not defined - then the list works as usual, in the mode of adding and editing data, if defined, then the list was called from the document to select the Customer.

Now go back to the document controller to implement the closure. But before we define the procedures for loading and saving the document. The logic of work here will be slightly different from working with reference books. As we remember, when creating a new document, we transmit nil and the document object itself when opening a View.not yet. If, when working with reference books, this did not interfere with us and we created the object itself just before recording, then we will create it for the document right away, since when editing rows of the table part we will need to specify a link to a specific document. In principle, nothing prevents us from using the same approach for reference books for uniformity, but in order to demonstrate different approaches, we will leave both options.

Thus, the procedure of “reading” data into form elements will look like this:
  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 } } 

Note: when creating the object, I immediately assigned the document the current date (the designer NSDate()returns the current date / time). And the data recording procedure:
  func saveOrder() { if let order = order { order.date = dataPicker.date order.made = switchMade.on order.paid = switchPaid.on CoreDataManager.instance.saveContext() } } 


Now let's finally implement the closure for the Customer sample, this is done quite simply:
  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! } } } } 

When switching to the Table View Controller, we define a handler, according to which, when choosing a Customer, we assign it to our document object, and also display the name of the Customer on the corresponding document control.

On this mechanism, the choice of the customer is completed, let's make sure that everything works as it should.



Now let's take a tabular part. Here everything should be familiar. Obviously, you need to create fetchedResultsControllerand implement protocols NSFetchedResultsControllerDelegate, UITableViewDataSourceand UITableViewDelegate.

But, just a minute, if we usefetchedResultsController, created in the same way as the previous one - we really get all the rows of the table part, but these will be the rows of all documents, and we need only the lines of the current document, the one with which the user works.

For this we need to add the appropriate filter in fetchRequest. This is done through the mechanism of predicates ( NSPredicate). We will talk about it a little more at the end of the article, but for now let's just add for our document ( Order.swift) a class function that will return the table part of the document as 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 } 

Pay attention to this line of code:

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

Here we set as the sort key the nested field of the object (“through the point”). Isn't that a great opportunity?

Now back in OrderViewController.swift, we need to declare a variable that will contain the tabular part and initialize it after initializing the document itself when loading the 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) } } } 

Immediately create a new View Controller to display the data line of the document and assign it a new class RowOfOrderViewController. Add the necessary navigation and control elements, Outlet and Action , implement the procedures for reading and writing the object. Also for the amount input field, set the numeric keypad ( Keyboard Type = Number Pad).



Now let's add a Segue named orderToRowOfOrder (by connecting the document and the newly created View Controller ) and implement the delegates of the necessary protocols in our document. All the same as in previous controllers, there is nothing fundamentally new here (just below I will give the full text of the module).

Also, let's add a button to add rows to the table part of the document. Here there is one nuance: if earlier, when creating a new object, nil was passed , and the object itself was created in another controller, then in the case of a row of the table part, we somehow need to “write” a specific document in it. This can be done in different ways, depending on the logic of the program. We will make the most obvious - we will pass not nil , but the object ( RowOfOrder), which we will immediately create and set in it a link to our document.
  @IBAction func AddRowOfOrder(sender: AnyObject) { if let order = order { let newRowOfOrder = RowOfOrder() newRowOfOrder.order = order performSegueWithIdentifier("orderToRowOfOrder", sender: newRowOfOrder) } } 

Please note: since we have a defined reverse relationship between entities in the data model, we don’t need to worry about creating it, it will be added automatically.



The text of the module 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() } } 


This completes the work directly with the document itself. It remains to finish with the View Controller , which displays information on the document line. Here we will use the exact same logic as with the header of the document. The choice of the Service will also be made through the capture context of the circuit.

Let's first add a Segue with a name rowOfOrderToServicesthat connects the View Controller document lines and a Table View Controller with a list of Services. We need to modify the Table View Controller a bit so that we can use the closure. First, add a closure variable:

 // ServicesTableViewController.swift // core-data-habrahabr-swift import UIKit import CoreData class ServicesTableViewController: UITableViewController, NSFetchedResultsControllerDelegate { typealias Select = (Service?) -> () var didSelect: Select? // … 

And, secondly, we will change the function of selecting the row of the list:

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

Return back to RowOfOrderViewControllerand implement the closure. Here everything is based on the same principle as when choosing a customer.
  @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 } } } } 


Text module 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() } } } 


Actually, everything! This completes the work with the document, let's check everything.



Important note!
We didn’t handle Cancel button clicks here , which led to the following situation. If we created a new document, and then decided not to save it and clicked Cancel , then it will remain hanging as a “draft” in our document journal, since from the current Core Data contextno one deleted it. You can return to it and continue to fill out, or you can delete it forcibly. But if you go back to the main menu, and then open the document journal again, there will be no drafts anymore, because when you open the journal, we read the data from the storage. All the same applies to the lines of the document. For our program, this behavior seems logical, well, at least - acceptable. But perhaps this behavior is not at all what you want in your program. In this case, you must implement your logic to respond to such events. Do not forget that in any case, the behavior of the program should be absolutely clear and transparent for the user.

Document Report


This section will be completely not big (in comparison with previous). We have already managed to get acquainted with it a bit NSFetchRequest, now let's take a closer look at it. Let's immediately create a new Table View Controller , create and assign a new class to it ( ReportTableViewControllerbased on UITableViewController).



We consider the work with NSFetchRequestan example of a simple report that will display a list of documents executed but not paid for, sorted by date. For this we will use two powerful tools that we have NSFetchRequest:

Start by sorting the data, look at the following definition:
 var fetchRequest = NSFetchRequest(entityName: "Order") // Sort Descriptor let sortDescriptor = NSSortDescriptor(key: "date", ascending: true) fetchRequest.sortDescriptors = [sortDescriptor] return fetchRequest }() 

Here we create a new data sorting object ( NSSortDescriptor), passing it to the constructor a string containing the name of the sort field, and specify the desired sort direction ( ascending: true- ascending, false- descending). Notice that NSFetchRequestwe pass the sort object as an array to the object. What does it mean?Yes, this is exactly - we can pass several sorting rules simultaneously in the form of an array.

Also recall that as a sort field, you can specify composite fields “through the point” (we did this when we sorted the rows of the table part of the document). Let's add a second sorting object in order to sort the documents within the date by the name of the Customer.
  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 }() 

Actually, this is all sorting. The only thing that still remind you - if you are actively using sorting, do not forget to think about the appropriateness of indexing the fields used.

We turn to the mechanism of predicates. It uses a fairly simple syntax, which is a bit like SQL-like queries. The predicate is created and used as follows:
  // Predicate let predicate = NSPredicate(format: "%K == %@", "made", true) fetchRequest.predicate = predicate 

A format string is passed to the constructor, followed by arguments. Depending on the format string, the number of parameters passed may vary. Let's take a closer look at the format string - it uses something like its own query language. " % K " - means the name of the field (property) of the object, " % @ " - the value of this field. Following are the arguments (actual values ​​that need to be substituted into the selection), strictly in the same order of succession. That is, this format string means the following: Order.made == true .

You can use not only the operation == , but < , > = , ! =and so on. You can also use keywords such as CONTAINS , LIKE , MATCHES , BEGINSWITH , ENDSWITH , as well as AND and OR . You can also use regular expressions. This is really a very powerful tool. I will not list here all the possible options, they are well represented in the official documentation of Apple . As arguments for the field name you can, as inNSSortDescriptor, use composite fields ("through the point"). But you cannot use several predicates at the same time; instead, you should use a more complex condition in a single predicate. With this in mind, the final definition of the predicate in our report will be as follows:
  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 }() 

It remains only to implement the UITableViewDataSource protocol (you already know how, nothing new here) and you can check it.

The text of the module 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 } } 




Everything works as it should, we received a list of documents according to the specified conditions.

Final Storyboard View


Conclusion


Using the example of a simple application, we examined all the main points of working with Core Dataand obtained, in a relatively short time, a fully functional application. The design, of course, at least asks for refinement, but this publication had a different goal. It is worth noting once again that all direct work with data, including the organization of the data warehouse and all possible checks for consistency, is hidden “under the hood” Core Data, we almost did not think about it, but worked with managed objects as with ordinary OOP objects.

I hope that I was able to quite clearly explain the basic techniques of working with Core Data, which, in my opinion, are necessary for any iOS developer. It is great if you stopped being afraid and, at least a little bit, fell in love Core Data. Thanks for attention.

This project is on github

Source: https://habr.com/ru/post/304586/


All Articles