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.
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.
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.
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.
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.
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.
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 | ||
Book Cover Image Url: |
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.
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! 🙂