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!
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 .

I'm sure you took on this challenge !
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 !
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