Using Core Data: Workshop 4

Today, you will set up the Add Book scene and it class files in the project. The app user will use it to add managed objects (books) in the persistent store. The user will also be able to return to the Book View Controller scene by clicking the navigation bar button that says, Book List.

Setup The Add Book Scene

Launch the CoreDataWorkshop you worked on last week. Drag a View Controller object from the Object Library and drop it on the storyboard canvas. Design its user interfaces to look like Figure A.

coredata-scene2
Figure A

Setup The Add Book Scene’s Class

Now that you’ve setup the Add Book scene, you need to set up the scene’s class.

  • Use Xcode’s template to add an Objective-C class in the project.
  • Make it a subclass of the View Controller class and name it AddBookViewController.
  • Connect the class to the Add Book scene by setting the scene’s Class attribute to AddBookViewController, in the Identity Inspector panel.
  • Connect each text field’s delegate property in Interface Builder.
  • Set the Image View control’s Mode property to Aspect Fit.
  • Connect the AddBookViewController scene to the BookViewController scene’s + button by creating a Push segue in Interface Builder.

Figure B shows what the storyboard should look like now in Interface Builder.

coredata-fig21
Figure B

At this point in time you need to add code in the AddBookViewController class files to make its view functional. Start by modifying the AddBookViewController.h file’s code to what’s shown below. Also, connect the view’s controls and the navigation bar’s Save button in Interface Builder.

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

@interface AddBookViewController : UIViewController

// A pointer for accessing the AppDelegate class properties and methods
@property (strong, nonatomic) AppDelegate *appDelegate;

// Properties for the view's controls
@property (strong, nonatomic) IBOutlet UITextField *bookTitle;
@property (strong, nonatomic) IBOutlet UITextField *authorName;
@property (strong, nonatomic) IBOutlet UITextField *bookCoverImageUrl;
@property (strong, nonatomic) IBOutlet UIImageView *bookCoverImageView;

// A property for holding the url of an image
@property (strong, nonatomic) NSString *noImageUrl;

// The navigation bar's Save button action method
- (IBAction)saveButtonClicked;

@end

Preload The Image View Control

What you need to do now is add code in the AddBookViewController class to extract the image from the url shown below, then load it in the view’s Image View control, so every time the user access the Add Book view, the Image View control will already have an image.

https://theapplady.net/wp-content/uploads/2014/05/noimage.png

To accomplish our objective, declare this property variable in the AddBookViewController.h file for holding above url.

@property (strong, nonatomic) NSString *noImageUrl;

Now, add this code in the ViewDidLoad method, which extract the image from from above url, then add it in the view’s Image View control. The last statement initialize the appDelegate property variable so we can access its properties and methods.

- (void)viewDidLoad
{
  self.noImageUrl = @"https://theapplady.net/wp-content/uploads/2014/05/noimage.png";
  UIImage *imageFromServer = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:self.noImageUrl]]];
  self.bookCoverImageView.image = imageFromServer;

  self.appDelegate = [[UIApplication sharedApplication] delegate];
}

If you were to run the app now, then access the Add Book view, the image extracted fro above url will look like this in the Image View control.

coredata-fig05a

The Image Url Extraction Method

You’ve just implemented code in the ViewDidLoad method to extract an image from the AppLady website, then load it in the view’s Image View control. What you need to do now is, extract the image from the URL the user provide in the Book Cover Image Url text field control, then load it in the image view control.

Here is the method to perform this operation. Declare and implement it in the AppDelegate files.

- (UIImage *) extractImageFromUrl:(NSString *)theUrl
{
  // Clean theUrl and and prepare it for validation
  NSString *cleanUrl = [theUrl stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
  NSURLRequest *urlRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:cleanUrl]];
  NSString *noImageUrl = @"https://theapplady.net/wp-content/uploads/2014/05/noimage.png";

  // Validate the the url
  bool validUrl = [NSURLConnection canHandleRequest:urlRequest];

  UIImage *imageFromServer;
  if(validUrl) {
    // the URL is valid, so set the imageFromServer variable with the image extract from the url passed to the method as parameter
    imageFromServer = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:cleanUrl]]];
  } else {
    // the URL is not valid, so set the imageFromServer variable with the image extracted from theapplady's website
    imageFromServer = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:self.noImageUrl]]];
  }

  // Returned the extracted image to the caller of this method
  return imageFromServer;
}

Once you’ve implemented the extractImageFromUrl: method in the AppDelegate.m file, you’ll have to use it in the AddBookViewController.m file’s touchesBegan: method. As you already know, that method is fired when the user tap the view’s background.

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
  // Initialize the Image View control's image property with the image extracted from the URL provided in the third Text Field control
  self.bookCoverImageView.image = [self.appDelegate extractImageFromUrl:self.bookCoverImageUrl.text];

  // Dismiss the keyboard
  [self.view endEditing:YES];
}

Now, to see the extractImageFromUrl: method in action, run the application, access the Add Book view and enter a book title and an author name in the first two text field controls. In the third text field control, enter this image url, the click the view’s background.

https://theapplady.net/wp-content/uploads/2013/10/oneforthemoney.jpg

The url’s image, oneforthemoney.jpg will appear in the view’s Image View control-see snap shut below.

coredata-fig05c

What you need to do now is use the extractImageFromUrl: method in the textFieldShouldReturn: method. That method is fired when the user tap the keyboard’s return key. Here is the code to implement the method in the AddBookViewController.m file.

-(BOOL)textFieldShouldReturn:(UITextField *)textField
{
  // Initialize the Image View control's image property with the image extracted from the URL provided in the third Text Field control
  self.bookCoverImageView.image = [self.appDelegate extractImageFromUrl:self.bookCoverImageUrl.text];

  // Dismiss the keyboard
  [textField resignFirstResponder];
  return YES;

Run the application again, enter the book title and author name shown in the snap shut below. Enter this url in the third text field control, then click the simulator’s return key.

https://encrypted.google.com/books/images/frontcover/wNlXt2VrumcC?fife=w300-rw

As you can see in the image below, the extractImageFromUrl: method you used in the textFieldShouldReturn: method did its job of extracting the url’s image and loaded it in the view’s Image View control.

coredata-fig20

The Save Button Method Code

You need to add code in the navigation bar’s Save button method to perform these tasks.

1. Clean the text field controls. This process involves removing white space and the new line character from the first two text field controls. Since entering an image url in the third text field is optional, we don’t clean it.

2. Validate the text field controls. This process involves checking to see if the first and second text field control is empty. If a text field is empty, then display a message in an Alert View.

3. Check for duplicate entry This process involves checking to see if the book title the user entered in the first text field control already exists in the database. If it does, then display a message in an Alert View.

4. Create a managed object in the persistent store. This process involves adding information the user entered in all text field controls, in the persistent store, which is a SQLite database file called, DataSchema.sqlite. By the way, the very first time you ran the CoreDataWorkshop application, a method of the Core Data stack created the persistent store file in the app’s sandbox.

Now, here is the code to add in the saveButtonClicked method, which implement tasks listed above.

- (IBAction)saveButtonClicked
{
  //1. Clean required text field controls
  NSString *cleanBookTitle = [self.bookTitle.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
  NSString *cleanAuthorName = [self.authorName.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];

  //2. Validate required text field controls
  if (cleanBookTitle.length == 0) {
    UIAlertView *alert = [[UIAlertView alloc]
    initWithTitle:@"Required Data Missing."
    message:@"Book Tittle"
    delegate: nil
    cancelButtonTitle: @"OK"
    otherButtonTitles: nil];
    [alert show];
  } else if (cleanAuthorName.length == 0) {
    UIAlertView *alert = [[UIAlertView alloc]
    initWithTitle:@"Required Data Missing."
    message:@"Author Name"
    delegate: nil
    cancelButtonTitle: @"OK"
    otherButtonTitles: nil];
    [alert show];
  } else {
    //3 Check for duplicate entry
    NSArray *books = [self.appDelegate retrieveBook:self.bookTitle.text];
    if ([books count] > 0) {
      // Display a warning message in an Alert View object
      UIAlertView *alert = [[UIAlertView alloc]
      initWithTitle:@"Book Not Saved"
      message:@"Sorry, unable to save the book because it already exists in the database."
      delegate: nil
      cancelButtonTitle: @"OK"
      otherButtonTitles: nil];
      [alert show];
    } else {
      //4 Create a managed object in the persistent store
      NSArray *bookObject = @[self.bookTitle.text, self.authorName.text, self.bookCoverImageUrl.text];
      [self.appDelegate createBook:(bookObject)];

      // Display a success message in an Alert View object
      UIAlertView *alert = [[UIAlertView alloc]
      initWithTitle:@"Book Save!"
      message:@"The book was saved successfully in the database."
      delegate: nil
      cancelButtonTitle: @"OK"
      otherButtonTitles: nil];
      [alert show];

      // Clear out the text fields controls
      self.bookTitle.text = nil;
      self.authorName.text = nil;
      self.bookCoverImageUrl.text = nil;
    }
  }
}

Once you are done entering code in the saveButtonClicked method, run the application. Use the Add Book view to add a book in the app’s database file. The first two images shows what you’ll see when you use the Add Book view to add a managed object in the persistent store. The third image shows what you’ll see when you fail to enter a title in the Book Title text field control. The fourth image shows what you’ll see when you try to add the same book title in the persistent store.

coredata-fig20 coredata-booksaved
coredata-fig05b coredata-booknotsaved-alert

The retrieveBook Method

You need to declare and implement this method in the AppDelegate class files. It was used on code line 26 in the saveButtonClicked method.

- (NSArray *)retrieveBook:(NSString *)bookTitle
{
  NSArray *result = [[NSArray alloc] init];
  NSError *error = nil;

  // Create a managed object context object
  NSManagedObjectContext *context = [self managedObjectContext];

  // Create a fetch request object
  NSFetchRequest *request = [[NSFetchRequest alloc] init];

  // Create an entity description object
  NSEntityDescription *entity = [NSEntityDescription entityForName:@"Book" inManagedObjectContext:context];

  // Add the entity description
  [request setEntity:entity];

  // Set up the predicate (fetch filter condition)
  NSPredicate *predicate = [NSPredicate predicateWithFormat:@"title = [c] %@", bookTitle];

  // Add the predicate to the fetch request
  [request setPredicate:predicate];

  // Execute the fetch request
  result = [context executeFetchRequest:request error:&error];

  if (!result) {
    // The fetch request failed, so display the error message the fetch request generated
    NSLog(@"%@",error);
  }

  return result;
}

In a nutshell, the retrieveBook: method’s job is to retrieve a single managed object (book object) from the persistent store, assign it to the results array, then return it to the caller of the method. Now, if the predicate statement was successful in finding a managed object in the persistent store file, for the book title provided in the bookTitle variable, the executeFetchRequest: method will assign a single managed object in the result array; otherwise, nothing is assign in the array.

The createBook: Method

Now, you need to declare and implement this method in the AppDelegate class files. It was used on code line 39 in the saveButtonClicked method. The createBook: method’s job is to add a managed object in the persistent store file.

- (void)createBook:(NSArray *)bookObject
{
  NSError *error = nil;

  // Create a managed object context
  NSManagedObjectContext *context = [self managedObjectContext];

  // Create a managed object in the context
  NSManagedObject *book = [NSEntityDescription insertNewObjectForEntityForName:@"Book" inManagedObjectContext:context];

  // Set the managed object's attributes in the context
  [book setValue:bookObject[0] forKey:@"title"];
  [book setValue:bookObject[1] forKey:@"author"];
  [book setValue:bookObject[2] forKey:@"imageUrl"];

  // Save the book object in the persistent store
  if (![context save:&error]) {
    // Unable to save, so display the error message the save: method generated
    NSLog(@"%@",error);
  }
}

Test The Save Button Code

Run the application once again and test the new code you entered in the saveButtonClicked method. Data you should enter in the view’s controls are shown in the Input column of the table below. Output you’ll see when you click the navigation bar’s Save button is shown in the Output column.

Input Output
coredata-fig20
Book Cover Image Url:
https://theapplady.net/wp-content/uploads/2013/10/oneforthemoney.jpg

coredata-booksavedcoredata-savedata2

Now, try to add the same book in the persistent store, the app won’t let you. Instead, you’ll see this Alert View in the simulator’s window.

coredata-booknotsaved-alert

That’s because the predicate statement in the retrieveBook: method’s filter condition contain the case insensitive character, which is the letter c in square brackets. The case insensitive character is telling the predicate to check the persistent store for a book title regardless of its case (capital or lower). The predicate will query the persistent store for the book title that contain lower and upper case letters.

For example, you originally entered the book title in the first text field control like this: When Harry Met Sally. Now, when you enter the book title in the text field like this: when harry met sally. The predicate statement in the retrieveBook: method will find a single managed object in the persistent store and return it. Now, this if statement in the saveButtonClicked method is responsible for displaying the Book Not Save Alert View in the simulator window.

if ([books count] > 0) {
  // Display a warning message in an Alert View object
  UIAlertView *alert = [[UIAlertView alloc]
  initWithTitle:@"Book Not Saved"
  message:@"Sorry, unable to save the book because it already exists in the database."
  delegate: nil
  cancelButtonTitle: @"OK"
  otherButtonTitles: nil];
  [alert show];
}

Refresh The Book List Table

After using the Add Book view to add one or more books in the app’s database file, you will have to click the navigation bar’s back button (Book List) to return to the Book List view. Currently, the new book you added in the persistent store doesn’t appear in the Book List view’s table. To rectify this problem, you’ll have to add code shown below in the BookListViewController.m file.

- (void)viewWillAppear:(BOOL)animated
{
  self.tableData = [self.appDelegate retrieveAllBooks];
  [self.tableView reloadData];
}

As you already know, the viewWillAppear method is fired just before the Book List view is loaded in the simulator’s window. You entered code in the method to fetch all managed objects from the persistent store file, add them in the table view control’s data source object, then refresh the view’s table view control. Now, when you add a new book in the database file, it will show up in the Book List view’s table view control.

That’s all we have to do in today’s workshop. Next, week, you’ll add code in the BookListViewController class to enable the user to delete a book from the app’s persistent store and to delete a row from the Book List view’s table view control. Until then, happy coding! 🙂