• 20 hours
  • Medium

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 4/24/18

Manage data objects

Log in or subscribe for free to enjoy all this course has to offer!

We have our entities set up, now let’s make them work for us. It all comes down to basic operations - adding, updating, deleting and simply accessing data - managed objects.

Implementing data manipulating functionality

Working with Dates

The date is an essential attribute of our logs. Not only are we going to use it to store this information as a reference, but we'll also use it to ensure we keep only one entry per day.

The date type in Swift is described by  Date class and a NS variation of it -  NSDate . Some of the methods we'll use from NS scope that will require us converting Date to NSDate

Date object consists of the date part and the time part. Technically as we generate date instances for each moment, chances are they will all be different. In order to ensure we only have one log per day we are going to use only the date part of it. We'll accomplish that by logging the time in the very beginning of each day. And since we are going to need this value on multiple occasions, let's create a calculated property for it in the data controller and call it  today . We could also use a couple of assisting methods: to convert a date to the beginning of the day and to display a date as a string:

    var today: Date {
        return startOfDay(for: Date())
    }
    
    func startOfDay(for date: Date) -> Date {
        var calendar = Calendar.current
        calendar.timeZone = TimeZone.current
        return calendar.startOfDay(for: date) // eg. yyyy-mm-dd 00:00:00
    }
    
    func dateCaption(for date: Date) -> String {
        let dateformatter = DateFormatter()
        dateformatter.dateStyle = .short
        dateformatter.timeStyle = .none
        dateformatter.timeZone = TimeZone.current
        
        return dateformatter.string(from: date)
    }

A few more new definitions here're coming along:

  • Calendar  - a structure used for various manipulations with dates related to the calendar. It assists with calculations of calendar units in context of particular date components, such as number of days in a particular month, or a month of a particular year - well, February is a good example of such an outlier ;).

  • DateFormatter  - is a class that supplies an array of helpful functions to format dates.

  • TimeZone  - is a class that implements functionality related to time zones.

Implement methods

Next, we'll break down our functionality to an array of simple methods we need to implement:

  • create a new Mood object

  • set new values to a Mood object

  • save context

  • fetch a Mood object by date

And implement them one by one.

Create a new object

To create a new object we simply request inserting a new NSManagedObject to the entity within the context:

    private func createMood() -> NSManagedObject? {
        if let entity = entity {
            return NSManagedObject(entity: entity, insertInto: context)
        }
        return nil
    }

This method will return us an optional of Mood managed object.

Update values of an object

Whether we created a new managed object or managed to fetch an existing one (previously logged), we can have a separate function that will be responsible for updating its values. In fact, only one value needs updating - the emotion:

    private func update(mood: NSManagedObject?, emotion: Emotion) {
        if let mood = mood {
            mood.setValue(emotion.rawValue, forKey: "emotion")
            mood.setValue(today, forKey: "createdAt")
        }
    }

The above implementation is pretty straight forward - we are take a managed object that is passed as a parameter and set its values to the new parameters.

Save context

All the data manipulations we perform with managed objects are preserved within the context. To commit that data to the store, we must save the context - we'll have a separate method for that as well:

    private func saveContext() {
        do {
           try context.save()
        }
        catch {
        }
    }

The above code executes a single method on the context variable - save(). In fact it tries to execute it and lets our code handle an error, should one occur during this attempt.

This is a new coding technique we are learning here - do/catch . do clause gets executed with try keyword, indicating that there's a chance it won't be possible.  In case an error occurs then catch handler is executed.

Fetch data

To fetch data, we need to learn the following objects:

  • NSFetchRequest  - an object responsible to make requests to the data store.

  • NSPredicate  - an object assisting a fetching request. It allows to specify fetching criteria.

And here's the implementation of fetching a mood object for a date:

    private func fetchMood(date: Date) -> NSManagedObject? {
        let request = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
        request.predicate = NSPredicate(format: "createdAt = %@", startOfDay(for: date) as NSDate)
        request.returnsObjectsAsFaults = false
        do {
            let result = try context.fetch(request) as! [NSManagedObject]
            return result.first
        } catch {
        }
        return nil
    }

Let's review the above:

  • We've create a request object for the entity.

  • Assigned to it a predicate object that specified criteria for createdAt attribute.

  • Attempted to fetch records matching criteria. Only one is expected, so we are taking the first one of the result array and returning it.

  • Not much to do if error occurs - so, we've got nothing in the catch clause. 

Logging mood

Now we are ready to complete the implementation of our public function to log an emotion of the day:

    func log(emotion: Emotion) {
        var mood = fetchMood(date: today)
        if mood == nil {
            mood = createMood()
        }
        update(mood: mood, emotion: emotion)
        saveContext()
    }

The code above is pretty self explanatory:

  • We're requesting an existing record for today

  • If log doesn't yet exist, we are creating a new one

  • Updating either en existing or new log 

  • Saving context

What left? - Testing :pirate:!

First and foremost, run the app and click mood buttons. Observe that the label caption changes as you tap around.

Testing deeper

Implement helpers

In order to perform some deeper tests, we'll need to implement a couple of helper members in our data controller class. Some of them will suit us in the future, some, will be used exclusively for testing.

Fetch number of records

First, we need to fetch our records. We may want all of them or some of them. To make this option flexible, let's implement a method that will take two optional parameters - to & from dates. This will cover a variety of cases:

  • If both parameters are nil, we'll return all the available records.

  • If both of them are provided - we'll return records starting from from date until to date inclusively.

  • If only from is specified, we'll return records logged on the from date and after.

  • If only to is specified, we'll return records logged on the to date and before:

    func logs(from: Date?, to: Date?) -> [NSManagedObject] {
        let request = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
        
        var predicate: NSPredicate?
        if let from = from, let to = to {
            predicate = NSPredicate(format: "createdAt >= %@ AND createdAt <= %@", startOfDay(for: from) as NSDate, startOfDay(for: to) as NSDate)
        }
        else if let from = from {
            predicate = NSPredicate(format: "createdAt >= %@ ", startOfDay(for: from) as NSDate)
        }
        else if let to = to {
            predicate = NSPredicate(format: "createdAt <= %@ ", startOfDay(for: to) as NSDate)
        }
        request.predicate = predicate
        
        let sectionSortDescriptor = NSSortDescriptor(key: "createdAt", ascending: false)
        request.sortDescriptors = [sectionSortDescriptor]
        
        request.returnsObjectsAsFaults = false
        do {
            let result = try context.fetch(request) as! [NSManagedObject]
            return result
        }
        catch {
        }
        return [NSManagedObject]()
    }

It will return a non-optional array of managed objects matching the parameters. In case we weren't able to fetch data, we'll return and empty array.

This code introduces another new object -  NSSortDescriptor  . It is used to define sorting parameters of the result.

Crear data

Next, we'll need a method that deletes all the records:

    func deleteAll() {
        // Create Fetch Request
        let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
        
        // Create Batch Delete Request
        let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
        
        do {
            try context.execute(batchDeleteRequest)
        } 
        catch {
        }
        saveContext()
    }

There's something new here - another new object -  NSBatchDeleteRequest  . As its name suggests, it's responsible for performing batch deleting operation. In our case, it deletes ALL the objects. We could also use a predicate object with batch deleting object to perform selective deletion.

And finally, since we're storing a single log per day, while going through this part of the course within a day, we'll get a single record stored. To generate more test data, we can implement a method that will fake user's logs autogenerating records for a specified number of days before today:

    func createDummyLogsFor(days: Int) {
        deleteAll()
        for i in 0 ..< days {
            let mood = createMood()
            let emotion = Emotion.happy
            if let mood = mood {
                var date = today
                date.addTimeInterval(-Double(i * 3600 * 24))
                mood.setValue(emotion.rawValue, forKey: "emotion")
                mood.setValue(date, forKey: "createdAt")
            }
        }
        saveContext()
    }

In the above, after clearing all the data, we are logging happy emotions for a number of days going backwards from today. We are altering the date to log by adding negative 3600 * 24 seconds to move one day backwards at each iteration.

To improve the function, implement a variation that randomly selects an emotion for each log :ange:.

You have no limits!
You have no limits!

I'm sure you took on this challenge :zorro:!

Print data

The last test method will be used to print logs in the console.  It will simply take an array of logs as a parameter and their attributes in the console:

    func printDetails(logs: [NSManagedObject]) {
        for log in logs as! [Mood]{
            print("\(dateCaption(for: log.createdAt!)) \(log.emotion)")
        }
    }

And to put it all together, here's the complete code of the data controller:

import Foundation
import UIKit
import CoreData

enum Emotion: String {
    case happy = "Happy"
    case neutral = "Neutral"
    case sad = "Sad"
    case snooze = "Snoozing"
}

class DataController {

    var entityName = "Mood"
    var context: NSManagedObjectContext
    var entity: NSEntityDescription?
    
    var today: Date {
        return startOfDay(for: Date())
    }
    
    func startOfDay(for date: Date) -> Date {
        var calendar = Calendar.current
        calendar.timeZone = TimeZone.current
        return calendar.startOfDay(for: date) // eg. yyyy-mm-dd 00:00:00
    }
    
    func dateCaption(for date: Date) -> String {
        let dateformatter = DateFormatter()
        dateformatter.dateStyle = .short
        dateformatter.timeStyle = .none
        dateformatter.timeZone = TimeZone.current
        
        return dateformatter.string(from: date)
    }
    
    init() {
        let appDelegate = UIApplication.shared.delegate as! AppDelegate
        context = appDelegate.persistentContainer.viewContext
        entity = NSEntityDescription.entity(forEntityName: entityName, in: context)
    }
    
    private func createMood() -> NSManagedObject? {
        if let entity = entity {
            return NSManagedObject(entity: entity, insertInto: context)
        }
        return nil
    }
    
    private func fetchMood(date: Date) -> NSManagedObject? {
        let request = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
        request.predicate = NSPredicate(format: "createdAt = %@", startOfDay(for: date) as NSDate)
        request.returnsObjectsAsFaults = false
        do {
            let result = try context.fetch(request) as! [NSManagedObject]
            return result.first
        } catch {
        }
        return nil
    }
    
    private func update(mood: NSManagedObject?, emotion: Emotion) {
        if let mood = mood {
            mood.setValue(emotion.rawValue, forKey: "emotion")
            mood.setValue(today, forKey: "createdAt")
        }
    }
    
    private func saveContext() {
        do {
           try context.save()
        }
        catch {
        }
    }
    
    func log(emotion: Emotion) {
        var mood = fetchMood(date: today)
        if mood == nil {
            mood = createMood()
        }
        update(mood: mood, emotion: emotion)
        saveContext()
    }
    
    func deleteAll() {
        // Create Fetch Request
        let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
        
        // Create Batch Delete Request
        let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
        
        do {
            try context.execute(batchDeleteRequest)
        }
        catch {
        }
        saveContext()
    }
    
    func createDummyLogsFor(days: Int) {
        deleteAll()
        for i in 0 ..< days {
            let mood = createMood()
            let emotion = Emotion.happy
            if let mood = mood {
                var date = today
                date.addTimeInterval(-Double(i * 3600 * 24))
                mood.setValue(emotion.rawValue, forKey: "emotion")
                mood.setValue(date, forKey: "createdAt")
            }
        }
        saveContext()
    }
    
    func logs(from: Date?, to: Date?) -> [NSManagedObject] {
        let request = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
        
        var predicate: NSPredicate?
        if let from = from, let to = to {
            predicate = NSPredicate(format: "createdAt >= %@ AND createdAt <= %@", startOfDay(for: from) as NSDate, startOfDay(for: to) as NSDate)
        }
        else if let from = from {
            predicate = NSPredicate(format: "createdAt >= %@ ", startOfDay(for: from) as NSDate)
        }
        else if let to = to {
            predicate = NSPredicate(format: "createdAt <= %@ ", startOfDay(for: to) as NSDate)
        }
        request.predicate = predicate
        
        let sectionSortDescriptor = NSSortDescriptor(key: "createdAt", ascending: false)
        request.sortDescriptors = [sectionSortDescriptor]
        
        request.returnsObjectsAsFaults = false
        do {
            let result = try context.fetch(request) as! [NSManagedObject]
            return result
        }
        catch {
        }
        return [NSManagedObject]()
    }
    
    func printDetails(logs: [NSManagedObject]) {
        for log in logs as! [Mood]{
            print("\(dateCaption(for: log.createdAt!)) \(log.emotion)")
        }
    }
}

We're now ready :pirate:!

Test-Test-Test

We'll test in two parts. First we need to verify that our logging for the day is working. And then we'll play with dummy data. We'll use viewDidLoad method of the view controller for our experiments.

Daily logging

Let's add the code to print all the stored logs:

let logs = dataController.logs(from: nil, to: nil)
dataController.printDetails(logs: logs)

To test the daily logs, simply run the app. At this point there should be nothing printed on viewDidLoad as we haven't saved any records yet.

Then, click on emoji-buttons, remember the last you clicked on. Terminate the app and run it again. You should have a single record printed in the console describing the last amoji-button you clicked.

If you repeat the test, you should be getting just a single record at any point, i.e.:

2/2/18 Optional("Happy")

If you repeat this further, you should still just get only one record printed.

Dummy data

Now let's insert some fake data, here's the play-code:

dataController.createDummyLogsFor(days: 5)
let logs = dataController.logs(from: nil, to: nil)
dataController.printDetails(logs: logs)

Let's run the app. Now, despite our latest log, we should only get fake data printed (the today's date should appear at the top):

2/2/18 Optional("Happy")
2/1/18 Optional("Happy")
1/31/18 Optional("Happy")
1/30/18 Optional("Happy")
1/29/18 Optional("Happy")

(in case you completed the challenge randomly picking fake emotions, your logs will look more exciting!)

Now you can click on one of the emotions, terminate the app, comment the dummy data creation line and run the app again. You fist line should be what clicked on during the last test:

2/2/18 Optional("Snoozing")
2/1/18 Optional("Happy")
1/31/18 Optional("Happy")
1/30/18 Optional("Happy")
1/29/18 Optional("Happy")

Great job! This is all for the moment. We'll explore some more elements of Core Data functionality in the following chapters of this course.

Core Data Alternatives

Core Data is a very powerful framework that allows you to manage object-graph. However, for simpler needs, less sophisticated frameworks are available that also use SQLite as a persistent store.

In following chapters of this course you'll learn how to integrate 3d party libraries that will allow you to explore various resources, that may include those managing stored data.

Let's Recap!

  • Creating an entry in the entity is done my creating a  NSManagedObject  object within that entity.

  • To save data in persistent store we must explicitly call safe() method on the context after updating managed objects.

  • To fetch data  NSFetchRequest  object is used.

  • NSPredicate  object is used to specify criteria that needs to be applied to select data.

  • Dedicated objects are available for batch data manipulations, amongst those are:

    • NSBatchDeleteRequest

    • NSBatchUpdateRequest

Example of certificate of achievement
Example of certificate of achievement