Swift Core Data From Scratch: Workshop 3

Today, you will implement these tasks in the SwiftCoreDataFromScratch iOS project you worked on last week:

  • Edit a Managed Object in the database
  • Prevent the user of the Add Book View from inserting duplicate managed objects in the app’s database file

Before You Begin

Now, before implementing above tasks in the project, do these tasks first:

  1. Use the Source Control menu to create a new branch called, “update-managed-object” from the fetch-and-display-managed-object branch.
  2. Click the BooksViewController.swift file and delete the debug code in the prepareForSegue() function.
  3. Import the CoreData module in the BookViewController.swift file.
  4. Create a constant called context and initialize it with the AppDelegate’s managedObjectContext variable.

The top portion of the BooksViewController class should look like this now:

import UIKit
import CoreData

class AddViewController: UIViewController {
    @IBOutlet weak var bookTitle: UITextField!
    @IBOutlet weak var authorName: UITextField!
    @IBOutlet weak var photoUrl: UITextField!
    @IBOutlet weak var imageView: UIImageView!
    @IBOutlet weak var textView: UITextView!
    let context = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext!
    var bookObject: Book!

Edit a Managed Object in The Database

To edit a managed object in the app’s database file, you’ll have to do the following:

  • Pass the table view cell’s managed object to the AddBookViewController class.
  • load the passed managed object’s properties in the Add Book View’s text fields.
  • Updated the passed managed object’s properties in the context, then save the context’s managed object in the app’s database file.

Believe it or not, you’ve already done the first task. It was done last week, when you entered code in the BooksViewController.swift file’s prepareForSegue() function to pass the selected managed object to the AddBookViewController class file’s bookObject variable.

Now, to load the managed object’s properties in the Add Book View’s text fields, add this code below existing code, in the AddBookViewController.swift file’s viewDidLoad() function:

// Load the view's text fields with the bookObject's properties
if bookObject != nil {
  bookTitle.text = bookObject.bookTitle
  authorName.text = bookObject.authorName
  photoUrl.text = bookObject.photoUrl
}

To save changes made in the text fields, in the database file; modify the AddBookViewController.swift file’s doneButtonTapped() code as shown below:

IBAction func doneButtonTapped(sender: AnyObject) {
    // Dismiss the keyboard
    bookTitle.resignFirstResponder()
    authorName.resignFirstResponder()
    photoUrl.resignFirstResponder()

    // Make sure required text fields aren't empty
    if bookTitle.text.isEmpty || authorName.text.isEmpty {
        textView.text = "Both the Book Title and the Author Name is required"

        // Re-initialize the view's text fields with the previously selected manage object's properties
        bookTitle.text = bookObject.bookTitle
        authorName.text = bookObject.authorName
        photoUrl.text = bookObject.photoUrl
        return // Don't exiecute remaining code in the function
    }

    if bookObject == nil {
        insertManagedObject()
    } else {
        updateManagedObject()
    }
}

A new function was called in the doneButtonTapped() function. Here’s the code to implement it:

// This function update an existing managedObject in the database then dismiss the view
func updateManagedObject() {
    // Reset the bookObject's properties with values entered in the text fields
    bookObject.bookTitle = bookTitle.text
    bookObject.authorName = authorName.text
    bookObject.photoUrl = photoUrl.text

    var savingError: NSError?

    // Update the managedObject in the database file
    if !context.save(&savingError) {
        // Display error message
        textView.text = "Failed to save the context with error = \(savingError?.localizedDescription)"
    } else {
        // Dismiss the AddViewController's view
        navigationController!.popViewControllerAnimated(true)
    }
}

Now, you have to add code in the BooksViewController.swift file to refresh the table view so changes made to the previously selected managed object appear in the table view. Here is the code to add in the BooksViewController.swift file:

override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(false)

    // Refresh the tableView
    tableView.reloadData()
}

Test The Edit Managed Object Code

Go ahead and run the app in the iPhone 5s Simulator and select the book you want to update. For example; click the first cell in the Books View’s table view. When you see the AddBookView, change one or all of the view’s text fields, then click the navigation bar’s Done button. The app updated the managed object in the database, then return you to the Books View.

scdfs-figure3-1 scdfs-figure4-2 scdfs-figure4-1

Preventing Duplicate Entry in The Database

Preventing the user of the Add Book View from inserting duplicate entry (managed object) in the app’s database file is something you should implement in your Core Data project. As of now, the SwiftCoreDataFromScratch project allow you to enter the same managed object in the database file. Take a look at the first image in above table to see what I mean.

Now, click the AddBookViewController.swift file and enter this code in the doneButtonTapped() function.

@IBAction func doneButtonTapped(sender: AnyObject) {
    // Dismiss the keyboard
    bookTitle.resignFirstResponder()
    authorName.resignFirstResponder()
    photoUrl.resignFirstResponder()

    // Make sure required text fields aren't empty
    if bookTitle.text.isEmpty || authorName.text.isEmpty {
        textView.text = "Both the Book Title and the Author Name is required"

        // Re-initial the view's text fields with the previously selected manage object's properties
        bookTitle.text = bookObject.bookTitle
        authorName.text = bookObject.authorName
        photoUrl.text = bookObject.photoUrl
        return // Don't execute remaining code in the function
    }

    // Check for duplicate bookTitle in the database
    if dbFieldExists(bookTitle.text, fieldNumber: 1) {
        textView.text = "Sorry, that Book Title already exists in the database."
        return // Don't execute remaining code in the function
    }

    // Check for duplicate photoUrl in the database
    if dbFieldExists(photoUrl.text, fieldNumber: 2) {
        textView.text = "Sorry, that Photo Url already exists in the database."
        return // Don't execute remaining code in the function
    }

    if bookObject == nil {
        insertManagedObject()
    } else {
        updateManagedObject()
    }
}

Notice how the dbFieldExists() function was called twice in the doneButtonTapped(). Here is the code to implement it:

func dbFieldExists(fieldName: String, fieldNumber: Int) -> Bool {
    // Empty out this object variable
    fetchedResultsController = nil

    // Setup the fetch request
    let fetchRequest = NSFetchRequest(entityName: "Book")
    fetchRequest.fetchLimit = 5

    // Setup the fetchRequest's predicate (query)
    if fieldNumber == 1 {
        fetchRequest.predicate = NSPredicate(format: "bookTitle = [cd] %@", fieldName)
    } else if fieldNumber == 2 {
        fetchRequest.predicate = NSPredicate(format: "photoUrl = [cd] %@", fieldName)
    }

    // Set up the fetchRequest's sortDescriptor
    let sortDescriptor = NSSortDescriptor(key: "bookTitle", ascending: true)
    fetchRequest.sortDescriptors = [sortDescriptor]

    // Pass the fetchRequest and the context as parameters to the fetchedResultController
    fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext:context,
        sectionNameKeyPath: nil, cacheName: nil)

    // Execute the fetchRequest or display an error message in the Debugger console
    var error: NSError? = nil
    if (!fetchedResultsController.performFetch(&error)) {
        println("Error: \(error?.localizedDescription)")
    }

    // Put the total number of managed objects the fetchRequest returned in a constant
    let managedObjectsFound = fetchedResultsController.fetchedObjects!.count
    if managedObjectsFound > 0 {
        // The bookTitle/photoUrl does exists in the database
        return true
    }

    // The bookTitle/photoUrl doesn't exists in the database
    return false
}

Above function query the database to see if the bookTitle or the photoUrl exists. Also, notice how the query condition was setup in the predicate statement. I’m pretty sure you’ll agree with me, this is a pretty nifty function.

Test The Code

Run the app again in the iPhone 5s simulator and test the dbFieldExists() function’s code. As you can see, the app won’t let you add the same book title or photo url in the database. Use the Source Control menu to commit changes you’ve made to the project, in the get repository.

Awesome! You’ve reached the end of the third workshop in the Swift Core Data From Scratch tutorial. Comments are welcomed. Next week, I will bring you the fourth workshop. Until then, happy coding! 🙂

  • Yash Tamakuwala

    So if I have a data set of say 1000 contacts to be stored in Core Data, the dbFieldExists will be called 2000 times. Doesn’t a better way exist?

  • Mark Lindamood

    In the “doneButtonTapped” function, the “dbFieldExists” function call seems to interfere with the “updateManagedObject” function call. Am I missing some code?