iOS with CoreData – Todo App

11 minutes, 42 seconds

Core Data is database system for iOS and macOS SDK. It’s build-in SDK so you can use it out of the box.

Note: this tutorial is revised to make more simpler factorized code inspired from Realm style tutorial. Play with source here.

Creating Add Item To Lists

1. Getting Started

Create an iOS app based on the Single View Application template, with details like the image below.

1

We will have Core Data extra features like Core Data codes inside AppDelegate.swift, and file called CoreDataApp.xcdatamodeld.

In case you are continuing from a project without ‘Use Core Data’ selected, you may create a CoreDataApp.xcdatamodeld file manually.

2. Create CoreData Helper Class

We will create a model class that handle the connection with Core Data database.

First of all, we would like to move the chunk of Core Data specific codes into a new class, lets call it CoreDataConnection.swift. Create the file as subclass of NSObject change the file with this code:


import UIKit
import CoreData

class CoreDataConnection: NSObject {
  
  static let sharedInstance = CoreDataConnection()
  
  static let kItem = "Item"
  static let kFilename = "CoreDataApp"
  
  // MARK: - Core Data stack
  
  lazy var persistentContainer: NSPersistentContainer = {

    let container = NSPersistentContainer(name: CoreDataConnection.kFilename)
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
      if let error = error as NSError? {

        fatalError("Unresolved error \(error), \(error.userInfo)")
      }
    })
    return container
  }()
  
  // MARK: - Core Data Saving support
  
  func saveContext () {
    let context = persistentContainer.viewContext
    if context.hasChanges {
      do {
        try context.save()
      } catch {
        let nserror = error as NSError
        fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
      }
    }
  }
  
  // MARK: - Adding More Helpers
  
  func createManagedObject( entityName: String )->NSManagedObject {
    
    let managedContext =
      CoreDataConnection.sharedInstance.persistentContainer.viewContext
    
    let entity =
      NSEntityDescription.entity(forEntityName: entityName,
                                 in: managedContext)!
    
    let item = NSManagedObject(entity: entity,
                               insertInto: managedContext)
    
    return item
    
  }
  
  
  func saveDatabase(completion:(_ result: Bool ) -> Void) {
    
    let managedContext =
      CoreDataConnection.sharedInstance.persistentContainer.viewContext
    
    do {
      try managedContext.save()
      
      completion(true)
      
    } catch let error as NSError {
      print("Could not save. \(error), \(error.userInfo)")
      completion(false)
    }
    
  }
  
  func deleteManagedObject( managedObject: NSManagedObject, completion:(_ result: Bool ) -> Void) {
    
    let managedContext =
      CoreDataConnection.sharedInstance.persistentContainer.viewContext
    
    managedContext.delete(managedObject)
    
    do {
      try managedContext.save()
      
      completion(true)
      
    } catch let error as NSError {
      print("Could not save. \(error), \(error.userInfo)")
      completion(false)
    }
    
  }
  

}

Now go back to AppDelegate.swift delete all functions that related to Core Data. Then replace this function.


func applicationWillTerminate(_ application: UIApplication) {
  // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
  // Saves changes in the application's managed object context before the application terminates.
  CoreDataConnection.sharedInstance.saveContext()
}

3. Setup the Storyboard

There is a single view controller at the storyboard.

1) Embed it into navigation controller.
2) Add a table view into ViewController make it pin zero to all edges.
3) Add also a ‘add item’ at the navigation bar.

2

4) Connect tableView to delegate and dataSource of view controller.

4. Setup the ViewController.swift

1) Connect tableView to IBOutlet.

  
@IBOutlet weak var tableView: UITableView!

2) Add an Array of String.


var items: [String] = []

3) Add UITableViewDataSource at the class declaration then implement it by adding this code.


// MARK: - UITableViewDataSource

func tableView(_ tableView: UITableView,
               numberOfRowsInSection section: Int) -> Int {
  return items.count
}
func tableView(_ tableView: UITableView,
               cellForRowAt indexPath: IndexPath)
  -> UITableViewCell {
    let cell =
      tableView.dequeueReusableCell(withIdentifier:"Cell",
                                    for: indexPath)
     
    cell.textLabel?.text = items[indexPath.row]
     
    return cell
}

We also have to create a IBAction from the navigation item button to addItem function.


@IBAction func addItem(_ sender: UIBarButtonItem) {
  let alert = UIAlertController(title: "New Item",
                                message: "Name of the new item",
                                preferredStyle: .alert)
  
  let cancelAction = UIAlertAction(title: "Cancel",
                                   style: .cancel)
  alert.addAction(cancelAction)
  
  let saveAction = UIAlertAction(title: "Save",style: .default) {
    [unowned self] action in
  
    guard let textField = alert.textFields?.first,
      let nameToSave = textField.text else {
        return
    }
    self.items.append(nameToSave)
    self.tableView.reloadData()
  }
  
  alert.addTextField()
  alert.addAction(saveAction)
  
  present(alert, animated: true)
}

You may run the project, add a new item and the item will show up in the list view.

However there is no persistant is adding yet to the app.

Setup Core Data

1. Modeling Your Data

First step is to create a managed object model or MOM, which describes the Data Model of your database.

1) Open the CoreDataApp.xcdatamodeld file, you will see a Data Model editor.

2) Click on Add Entity on the lower-left to create a new entity. Double-click the new entity and change its name to Item.

3) While the ‘Item’ entity selected, add an attibute called ‘title‘ with type of String.

4) While the ‘Item’ entity selected, add an attribute called ‘progress‘ with type of Double.

Your CoreDataApp.xcdatamodeld file will look like this.

A

Xcode Automatic Subclass Generation

This feature is new for iOS 10. You can generate the NSManagedObject for the entity you have on model editor, just enable it by looking at the Data Model Inspector. Configure to this:

A

Class: Item
Module: Current Product Module
Codegen: Class definition.

What this mean that you can infer you NSManagedObject to the generated class like this code:



let item = itemsFromCoreData[indexPath.row] as! Item
cell.textLabel?.text = item.title

2. Saving to Core Data

1) Link CoreData framework to ViewController.swift.


import CoreData

2) Add this var as shorthand at top inside the class.


var coreData = CoreDataConnection.sharedInstance

3) Add this new variable which the item is NSManagedObject. This is will automatically get the latest list from the database.


var itemsFromCoreData: [NSManagedObject] {
    
    get {
      
      var resultArray:Array<NSManagedObject>!
      let managedContext = coreData.persistentContainer.viewContext
      //2
      let fetchRequest =
        NSFetchRequest<NSManagedObject>(entityName: CoreDataConnection.kItem)
      //3
      do {
        resultArray = try managedContext.fetch(fetchRequest)
      } catch let error as NSError {
        print("Could not fetch. \(error), \(error.userInfo)")
      }
      
      return resultArray
    }
    
  }

4) Change the save function.


@IBAction func addItem(_ sender: UIBarButtonItem) {
  let alert = UIAlertController(title: "New Item",
                                message: "Name of the new item",
                                preferredStyle: .alert)
  
  let cancelAction = UIAlertAction(title: "Cancel",
                                   style: .cancel)
  alert.addAction(cancelAction)
  
  let saveAction = UIAlertAction(title: "Save",style: .default) {
    [unowned self] action in
  
    guard let textField = alert.textFields?.first,
      let nameToSave = textField.text else {
        return
    }
    self.saveToCoreData(nameToSave)
    
  }
  
  alert.addTextField()
  alert.addAction(saveAction)
  
  present(alert, animated: true)
}

4) Add saveToCoreData function


func saveToCoreData(_ title: String){

  let item = coreData.createManagedObject(entityName: CoreDataConnection.kItem) as! Item
    
  item.title = title
  
  coreData.saveDatabase { (success) in
    
    if (success){
      self.tableView.reloadData()
    }
    
  }
  
}

5) You will also need to update the UITableViewDataSource implementation to this:


// MARK: - UITableViewDataSource

func tableView(_ tableView: UITableView,
               numberOfRowsInSection section: Int) -> Int {
  return itemsFromCoreData.count
}

func tableView(_ tableView: UITableView,
               cellForRowAt indexPath: IndexPath)
  -> UITableViewCell {
  
    let cell =
      tableView.dequeueReusableCell(withIdentifier:"Cell",
                                    for: indexPath)
    
    let item = itemsFromCoreData[indexPath.row] as! Item
    
    cell.textLabel?.text = item.title
    cell.detailTextLabel?.text = "\(item.progress)"
    
    return cell
    
}

And done, you can add item and the item will persisted even you terminate the app.

Simulator Screen Shot 18 Jan, 2017, 12.12.35 AM.png

Update the Progress

Add the UITableViewDelegate at the class definition. And we will implementing for responding to table select. It will toggle the progress from 0.0 to 1.0 when user tap on the cell.


// MARK: - UITableViewDelegate

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  tableView.deselectRow(at: indexPath, animated: true)
      
  let item = itemsFromCoreData[indexPath.row] as! Item
  
  if (item.progress == 0){
    item.progress = 1
  }else{
    item.progress = 0
  }
  
  coreData.saveDatabase { (success) in
    if (success) {
      tableView.reloadRows(at: [indexPath], with: .automatic)
    }
  }
  
}

Delete the Item

To delete an item we will want to enable the UITableView to be able to response to edit like it can be swiped to left.

To do this, we have to use the UITableViewDelegate implementation like below.


func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
  return true
}

func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
  
  if (editingStyle == .delete){
    
    let item = itemsFromCoreData[indexPath.row] as! Item
    
    coreData.deleteManagedObject(managedObject: item, completion: { (success) in
      if (success){
        
        tableView.deleteRows(at:[indexPath], with: .automatic)
        
      }
    })
  }
}

Sorting

To sort the result of your list can be done by tweaking from the var with get.

We add a NSSortDescriptors here to sort by decending name.


var itemsFromCoreData: [NSManagedObject] {
  
  get {
    
    var resultArray:Array<NSManagedObject>!
    let managedContext = coreData.persistentContainer.viewContext
    let fetchRequest =
      NSFetchRequest<NSManagedObject>(entityName: CoreDataConnection.kItem)
    
    let sortDescriptor = NSSortDescriptor(key:"title", ascending: true)
    
    fetchRequest.sortDescriptors = [sortDescriptor]
    
    do {
      resultArray = try managedContext.fetch(fetchRequest)
    } catch let error as NSError {
      print("Could not fetch. \(error), \(error.userInfo)")
    }
    
    return resultArray
  }
  
}

Using App Group

There is time when we want to share the Core Data database with other extensions. Inside CoreDataConnection.swfit add the kGroup identifier and also change the persistentContainer like this.


static let kGroup = "group.com.irekasoft.myApp" // MARK: - Core Data stack lazy var persistentContainer: NSPersistentContainer = { let container = NSPersistentContainer(name: CoreDataConnection.kFilename) container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: try! FileManager.default.containerURL(forSecurityApplicationGroupIdentifier:kGroup)!.appendingPathComponent("Data.sqlite"))] container.loadPersistentStores(completionHandler: { (storeDescription, error) in if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") } }) return container }()

Source Code:

Download source from Github Link.

References:

Apple, Core Data 2016