Learning Core Data for iOS (2014)

16. Web Service Integration

Everybody is a genius. But if you judge a fish by its ability to climb a tree, it will live its whole life believing that it is stupid.

Albert Einstein

In Chapter 14, “iCloud,” and Chapter 15, “Taming iCloud,” Grocery Dude’s Core Data stack was integrated with iCloud. A key limitation of iCloud, however, is that its data is constrained to one iCloud account. Because iCloud accounts are deeply intertwined with many aspects of user devices, it is not practical or recommended to share iCloud accounts. This means that iCloud cannot be used to collaborate. For example, assume a husband and wife wanted to contribute to the same shopping list. This isn’t currently possible with iCloud. The bottom line is that alternative backend systems need to be considered instead. There are several options available, and you should evaluate them on their own merits. In the interest of not reinventing the wheel, it is recommended that you avoid provisioning your own servers, databases, security, and everything else involved with managing a backend system. This means you can instead rely on tried-and-true frameworks developed by service providers such as StackMob. This chapter will explain and demonstrate the steps to integrate Core Data with StackMob. The decision to use StackMob is driven from its close alignment with Core Data.

Introducing StackMob

StackMob is a company that provides an enterprise-class Backend-as-a-Service (BaaS), which allows devices to share data so long as they can connect to a REST API and authenticate with OAuth2. The StackMob iOS SDK is so tightly integrated with Core Data that once you’ve replaced the persistent store with a StackMob store, you’re almost ready to go.


Note

REST stands for Representational State Transfer and is the software architecture approach for intersystem communication used by StackMob. Think of REST as “The Internet” for application components. If one area of a system needs to access a resource in another area of the system, it uses a URL like you do when visiting a website. This client/server model works well because it allows distributed systems to remain modular. This means each component is self-contained and independently upgradable without fear of breaking something up or downstream.


StackMob will use the existing Core Data Managed Object Model; however, you’ll need to configure it so that it works with StackMob. Once the local model is updated, you’ll need to configure equivalent StackMob schemas. This is so their backend system knows what objects to expect from the model. A StackMob schema is equivalent to a Core Data entity. To create the schemas, all you need to do is create a managed object for each entity, and the equivalent schemas will be generated automatically.

The StackMob client replaces the need for a persistent store coordinator and provides a managed object context and caching option. At the backend, StackMob will store everything except binary data, which is instead stored in Amazon’s Simple Storage Service (S3). Using the S3 service costs money, so photos and thumbnails will not be implemented in this chapter in order to keep the sample project free. An overview of Core Data integration with StackMob is shown in Figure 16.1.

Image

Figure 16.1 StackMob overview

To integrate Core Data with StackMob, you simply add the StackMob iOS SDK and supporting frameworks to an Xcode project. Once everything is integrated, an instance of SMClient can be created and the managed object model supplied to the client in order to get back a persistent store and context to work with. A cache policy is also set to ensure that all fetches are cached in a local store that is available without reliance on network connectivity.


Note

StackMob integration into Grocery Dude will branch off into a new project called Grocery Cloud. The creation of this new base project is covered in Appendix B, “Preparing Grocery Cloud for Chapter 16.” Follow the instructions in Appendix B if you would prefer to create the base project yourself. Alternatively, you may download, unzip, and open the base project from http://www.timroadley.com/LearningCoreData/GroceryCloud-AfterAppendixB.zip. Any time you start using an Xcode project from a ZIP file, it’s good practice to clickProduct > Clean. This practice ensures there’s no residual cache from previous projects using the same name.


The StackMob SDK

To add StackMob support, the first thing you’ll need to do is download the StackMob iOS SDK. At the time of writing, the latest version is 2.1.1. There are also several supporting frameworks that Grocery Cloud must first link to in order for StackMob to function correctly.

Update Grocery Cloud as follows to add the required SDK and frameworks:

1. Download and extract the latest iOS SDK from the following URL: https://developer.stackmob.com/sdks. Follow their installation steps, which should resemble steps 2 to 5.

2. Drag the downloaded StackMob directory into the root of the Grocery Cloud Xcode project and ensure that “Copy items into destination group’s folder” is ticked. Also, ensure that “Create groups for any added folders” is selected and that the Grocery Cloud target is ticked before clicking Finish.

3. Select the General tab of the Grocery Cloud target and scroll down to the Linked Frameworks and Libraries section. Add the following frameworks, as shown in Figure 16.2:

image MobileCoreServices

image SystemConfiguration

image Security

image CoreLocation

Image

Figure 16.2 Additional frameworks required to support StackMob

4. Add -ObjC in the Other Linker Flags section found in the Build Settings tab of the Grocery Cloud target, as shown in Figure 16.3. (Tip: You may need to click All to see all the settings and then search for Other Linker Flags.)

Image

Figure 16.3 ObjC linker flag is required by the StackMob iOS SDK.

5. Add the following lines to /Supporting Files/Grocery Cloud-Prefix.pch before the last #endif:

#import <SystemConfiguration/SystemConfiguration.h>
#import <MobileCoreServices/MobileCoreServices.h>

As mentioned previously, interaction with StackMob web services will be via a StackMob client. Before a StackMob client can be used, an application needs to be defined on the StackMob servers.

Creating a StackMob Application

To create an application on StackMob, you’ll need a StackMob or GitHub account. Head over to www.stackmob.com and create an account now. Once you’ve created an account, you’ll be redirected to a page used to set up a new application.

Create an application with the name grocery_cloud. You’ll be taken to a tutorial, which you may skip. Navigate to the StackMob Dashboard and then select Schema Configuration. This area allows you to view the existing backend schema for Grocery Cloud. At this point there’s only a user schema, as shown in Figure 16.4.

Image

Figure 16.4 A StackMob schema

Managed Object Model Preparation

As shown previously in Figure 16.1, a Managed Object Model is still used to define the application’s data structure when using StackMob. However, mandatory changes are required to the existing model before integration can be achieved.

Since version 2 of StackMob’s iOS SDK, a feature called Offline Sync has been available. This feature allows an application to remain usable when network connectivity is unavailable. Offline writes are cached, and once connectivity returns, changes can be synchronized with the server again.

To support the Offline Sync feature, two new date attributes called createddate and lastmoddate are required in each entity. These attributes are used for conflict resolution in cases when an offline device has changed data that has also been modified by other means. For brevity, these attributes will be added directly to each entity to avoid the need for a mapping model. In applications that have not yet been released to the public, it is recommended that a new entity called StackMob be introduced as a parent to all existing entities. This simplifies the model design, which is explained in detail in StackMob’s own documentation.

In addition to the new date attributes, a new string attribute is required in each entity for the object’s StackMob primary key ID. The name of this attribute will be a combination of the containing entity name (all in lowercase) with a suffix of _id. For example, the Item entity requires a new attribute called item_id.

Update Grocery Cloud as follows to configure the model for StackMob integration:

1. Select Model.xcdatamodeld.

2. Click Editor > Add Model Version....

3. Click Finish to accept Model 10 as the version name.

4. Ensure Model 10.xcdatamodel is selected.

5. Add an attribute called createddate to the Item entity and then set its type to Date.

6. Add an attribute called lastmoddate to the Item entity and then set its type to Date.

7. Copy the createddate and lastmoddate attributes from the Item entity to the Item_PhotoLocation, and Unit entities. Depending on your version of Xcode, the Model Editor may need to be in Table Editor Style for attribute copy and paste to work.

8. Add a completely lowercase String attribute called entityname_id to every entity. For example, in the Item entity, create a String attribute called item_id. Use Figure 16.5 as a guide to the exact attribute name required in each entity.

Image

Figure 16.5 StackMob compatible model

Another entity called User is required for authentication.

Update Grocery Cloud as follows to add a User entity and start using Model 10:

1. Add an entity called User.

2. Add an attribute called user_id to the User entity and then set its type to String.

3. Add an attribute called username to the User entity and then set its type to String.

4. Add an attribute called createddate to the User entity and then set its type to Date.

5. Add an attribute called lastmoddate to the User entity and then set its type to Date.

6. Select all entities in Model 10 and generate NSManagedObject subclasses, replacing existing files (via Editor > Create NSManagedObject Subclass...). Ensure the Grocery Cloud target is ticked before clicking Create.

7. Repeat step 6 to ensure relationships are correctly generated in the subclass files.

8. Select Model.xcdatamodeld and then set the current model to Model 10 using File Inspector (Option+image+1). The expected result is shown in Figure 16.5.


Note

Sometimes, creating NSManagedObject subclass files causes the same file to be linked twice in Xcode. If this happens, it will cause a Mach-O Linker Error at runtime, and you’ll need to delete one of the duplicate references of each subclass file.


Configuring a StackMob Client

Interaction with the StackMob web service occurs via a StackMob client (that is, SMClient). A StackMob client requires that a public key be provided when an instance of SMClient is created. You can find the development and production keys for a StackMob application using the dashboard shown previously in Figure 16.4. From the StackMob Dashboard, click Home to view the public keys for your own grocery_cloud application.

To support StackMob, a property for a client instance will be required in CoreDataHelper.h, in addition to a new property for the special StackMob persistent store. This store behaves like any other persistent store. To save to it, you’ll just access the context for the current thread through the StackMob client. Listing 16.1 shows the two new properties required for StackMob integration.

Listing 16.1 CoreDataHelper.h


@property (retain, nonatomic) SMClient                  *stackMobClient;
@property (retain, nonatomic) SMCoreDataStore           *stackMobStore;


Update Grocery Cloud as follows to integrate with StackMob:

1. Add #import "StackMob.h" to the top of CoreDataHelper.h.

2. Add the properties from Listing 16.1 to CoreDataHelper.h beneath the existing properties.

The next step is to update CoreDataHelper.m to use the StackMob client. This client will contain the StackMob managed object context(s) and persistent store coordinator. Listing 16.2 shows the code involved in configuring the StackMob client.

Listing 16.2 CoreDataHelper.m: init


- (id)init {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    self = [super init];
    if (!self) {return nil;}

    _model = [NSManagedObjectModel mergedModelFromBundles:nil];

    // Use StackMob Cache Store in case network is unavailable
    SM_CACHE_ENABLED = YES;

    // Verbose logging
    SM_CORE_DATA_DEBUG = NO;

    _stackMobClient  =     // APIVersion 0 = Dev, 1 = Prod
    [[SMClient alloc] initWithAPIVersion:@"0" publicKey:@"YOUR_APP_KEY"];
    _stackMobStore =
    [_stackMobClient coreDataStoreWithManagedObjectModel:_model];

    __weak SMCoreDataStore *cds = _stackMobStore;
    [_stackMobClient.session.networkMonitor
        setNetworkStatusChangeBlockWithCachePolicyReturn:^SMCachePolicy(
            SMNetworkStatus status
        ){
        if (status == SMNetworkStatusReachable) {
            [cds syncWithServer];
            return SMCachePolicyTryNetworkElseCache;
        } else {
            return SMCachePolicyTryCacheOnly;
        }
    }];

    _context = [_stackMobStore contextForCurrentThread];

    _importContext =
    [[NSManagedObjectContext alloc]
                          initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [_importContext performBlockAndWait:^{
        [_importContext setParentContext:_context];
        [_importContext setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];
        [_importContext setContextShouldObtainPermanentIDsBeforeSaving:YES];
        [_importContext setUndoManager:nil]; // the default on iOS
    }];


    _sourceContext =
    [[NSManagedObjectContext alloc]
                          initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [_sourceContext performBlockAndWait:^{
        [_sourceContext setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];
        [_sourceContext setParentContext:_context];
        [_sourceContext setContextShouldObtainPermanentIDsBeforeSaving:YES];
        [_sourceContext setUndoManager:nil]; // the default on iOS
    }];
    return self;
}


Mostly the first half of the init method of CoreDataHelper.m has changed, as the StackMob client now handles the persistent store coordination. The client is configured with an API version of 0 (Development) as opposed to 1 (Production). No public key is listed in Listing 16.2 because you will need to substitute YOUR_APP_KEY with your own application’s public key, available from the StackMob Dashboard.

Once a cache policy has been set based on reachability options, the remaining code in the init method hasn’t changed too much. A new context setting called setContextShouldObtainPermanentIDsBeforeSaving is configured on the import and source contexts as recommended by StackMob, which ensures that permanent IDs for newly inserted objects are created in these contexts. Any time access to a StackMob context is required, it should be accessed via the purpose-built contextForCurrentThread method available via the StackMob store.

Update Grocery Cloud as follows to configure the StackMob client:

1. Replace the init method of CoreDataHelper.m with the code from Listing 16.2.

2. Replace YOUR_APP_KEY with your own application’s public key, available via the StackMob Dashboard.

3. Comment out all code in the setupCoreData method of CoreDataHelper.m. This code is no longer required but is left in place so you can more easily experiment with it later.

Saving

There are two ways to save using StackMob: synchronously and asynchronously. To ensure the application remains responsive, asynchronous saving is recommended where possible. The existing saveContext and backgroundSaveContext methods will be updated to save a StackMob context.Listing 16.3 shows the code involved in these updated methods.

Listing 16.3 CoreDataHelper.m: SAVING


- (void)saveContext {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    NSManagedObjectContext *stackMobContext =
    [_stackMobStore contextForCurrentThread];
    if (!stackMobContext) {
        NSLog(@"StackMob context is nil, so FAILED to save");
        return;
    }

    NSError *error;
    [stackMobContext saveAndWait:&error];
    if (!error) {
          NSLog(@"SAVED changes to StackMob store (in the foreground)");
    } else {
        NSLog(@"FAILED to save changes to StackMob store (in the foreground): %@"
                                                                        , error);
    }
}
- (void)backgroundSaveContext {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    NSManagedObjectContext *stackMobContext =
    [_stackMobStore contextForCurrentThread];
    if (!stackMobContext) {
        NSLog(@"StackMob context is nil, so FAILED to save");
        return;
    }

    [stackMobContext saveOnSuccess:^{
        NSLog(@"SAVED changes to StackMob store (in the background)");
    } onFailure:^(NSError *error) {
        NSLog(@"FAILED to save changes to StackMob store (in the background): %@"
                                                                        , error);
    }];
}


Update Grocery Cloud as follows to enable StackMob context saves:

1. Replace the saveContext and backgroundSaveContext methods of CoreDataHelper.m with the equivalent methods from Listing 16.3.

2. Replace [[self cdh] backgroundSaveContext]; in the applicationDidEnterBackground and applicationWillTerminate methods of AppDelegate.m with [[self cdh] saveContext];.

3. Add [cdh saveContext]; to the bottom of the textFieldDidEndEditing method of ItemVC.m.

Any time a new object is created, it should be given a primary key and the object saved immediately. Each method that inserts objects in Grocery Cloud has slightly different code, so this is spelled out in Listing 16.4. The expected placement of this code will be immediately after the existing call to insertNewObjectForEntityForName.

Listing 16.4 Grocery Cloud Object Insertions


// In PrepareTVC.m prepareForSegue
[newItem setValue:[newItem assignObjectId]
           forKey:[newItem primaryKeyField]];
[cdh saveContext];

// In LocationsAtHomeTVC.m prepareForSegue
[newLocationAtHome setValue:[newLocationAtHome assignObjectId]
                     forKey:[newLocationAtHome primaryKeyField]];
[cdh saveContext];

// In LocationsAtShopTVC.m prepareForSegue
[newLocationAtShop setValue:[newLocationAtShop assignObjectId]
                     forKey:[ newLocationAtShop primaryKeyField]];
[cdh saveContext];

// In UnitsTVC.m prepareForSegue
[newUnit setValue:[newUnit assignObjectId]
           forKey:[newUnit primaryKeyField]];
[cdh saveContext];

// In ItemVC.m ensureItemHomeLocationIsNotNull
[locationAtHome setValue:[locationAtHome assignObjectId]
                  forKey:[ locationAtHome primaryKeyField]];
[cdh saveContext];

// In ItemVC.m ensureItemShopLocationIsNotNull
[locationAtShop setValue:[locationAtShop assignObjectId]
                  forKey:[locationAtShop primaryKeyField]];
[cdh saveContext];

// In ItemVC.m imagePickerController:didFinishPickingMediaWithInfo
[newPhoto setValue:[newPhoto assignObjectId]
            forKey:[newPhoto primaryKeyField]];
[cdh saveContext];


Update Grocery Cloud as follows to ensure all new objects are given a primary key:

1. Add the line of code for each method shown in Listing 16.4 to the line after the call to insertNewObjectForEntityForName found in the same method.

Note that some classes out of scope of this chapter have not been updated (for example, CoreDataImporter).

Underlying Changes

At the time of writing, there’s no StackMob notification that can be observed to trigger a table view refresh in response to a change in underlying data. For example, with iCloud, NSPersistentStoreDidImportUbiquitousContentChangesNotification can be observed. As a workaround, aviewDidAppear method will be added to trigger a refresh on each table view. Listing 16.5 shows the code involved.

Listing 16.5 PrepareTVC.m, ShopTVC.m, UnitsTVC.m, LocationsAtHomeTVC.m, and LocationsAtShopTVC.m: viewWillAppear


- (void)viewDidAppear:(BOOL)animated {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    [super viewDidAppear:animated];
    [self configureFetch];
    [self performFetch];
}


Update Grocery Cloud as follows to ensure the table views are refreshed with the latest data when they appear:

1. Comment out [self performFetch]; in the viewDidLoad method of all table view controller classes (that is, PrepareTVC.m, ShopTVC.m, UnitsTVC.m, LocationsAtHomeTVC.m, and LocationsAtShopTVC.m).

2. Add the method from Listing 16.5 to the top of the VIEW section of all table view controller classes (that is, PrepareTVC.m, ShopTVC.m, UnitsTVC.m, LocationsAtHomeTVC.m, and LocationsAtShopTVC.m).

A UIRefreshControl will be added to CoreDataTVC later in the chapter so the user can manually trigger a refresh.

Automatic Schema Generation

The backend StackMob servers require that a schema be configured for each entity that needs to be synchronized. The naming convention between an entity and its equivalent schema differs slightly. These differences are explained in the “StackMob Core Data Coding Practices” section of the StackMob iOS SDK Reference, which is available from the StackMob website. It’s not too critical that you learn the differences at this stage because the schemas are generated automatically during the development phase. Generating a schema is as easy as creating a new object because that’s exactly how schemas (and relationships) are generated. Listing 16.6 shows a new method that will be used as a one-off to generate the equivalent StackMob schemas for each entity.

Listing 16.6 AppDelegate.m: generateStackMobSchema


- (void)generateStackMobSchema {

if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    CoreDataHelper *cdh = [self cdh];
    NSManagedObjectContext *stackMobContext =
    [cdh.stackMobStore contextForCurrentThread];

    // Create new objects for each entity
    LocationAtHome *locationAtHome =
    [NSEntityDescription insertNewObjectForEntityForName:@"LocationAtHome"
                                  inManagedObjectContext:stackMobContext];
    LocationAtShop *locationAtShop =
    [NSEntityDescription insertNewObjectForEntityForName:@"LocationAtShop"
                                  inManagedObjectContext:stackMobContext];
    Unit *unit =
    [NSEntityDescription insertNewObjectForEntityForName:@"Unit"
                                  inManagedObjectContext:stackMobContext];
    Item *item =
    [NSEntityDescription insertNewObjectForEntityForName:@"Item"
                                  inManagedObjectContext:stackMobContext];
    Item_Photo *item_photo =
    [NSEntityDescription insertNewObjectForEntityForName:@"Item_Photo"
                                  inManagedObjectContext:stackMobContext];

    // Set Primary Key Fields
    [locationAtHome setValue:[locationAtHome assignObjectId]
                      forKey:[locationAtHome primaryKeyField]];
    [locationAtShop setValue:[locationAtShop assignObjectId]
                      forKey:[locationAtShop primaryKeyField]];
    [unit           setValue:[unit assignObjectId]
                      forKey:[unit primaryKeyField]];
    [item           setValue:[item assignObjectId]
                      forKey:[item primaryKeyField]];
    [item_photo     setValue:[item_photo assignObjectId]
                      forKey:[item_photo primaryKeyField]];

    // Give each attribute a value so the schema is generated automatically
    locationAtHome.storedIn = @"Fridge";
    locationAtShop.aisle = @"Cold Section";
    unit.name = @"L";
    item.name = @"Milk";
    item.collected = [NSNumber numberWithBool:NO];
    item.listed = [NSNumber numberWithBool:YES];
    item.quantity = [NSNumber numberWithInt:1];

    // sectionNameKeyPath WORKAROUND (See Appendix B)
    item.storedIn = @"Fridge";
    item.aisle = @"Cold Section";

    // Always save objects before relationships are created to avoid corruption
    [cdh saveContext];

    // Create relationships and then save again
    item.unit = unit;
    item.locationAtHome = locationAtHome;
    item.locationAtShop = locationAtShop;
    item.photo = item_photo;
    [cdh saveContext];
}


The code in the generateStackMobSchema is nothing that hasn’t been demonstrated previously in this book. An object is created for each entity, and the attributes for each are then populated to ensure they’re generated within a StackMob schema.

Update Grocery Cloud as follows to generate a StackMob schema for each entity:

1. Add #import "Item_Photo.h" to the top of AppDelegate.m.

2. Add the code from Listing 16.6 to the bottom of AppDelegate.m before @end.

3. Add [self generateStackMobSchema]; to the bottom of the didFinishLaunchingWithOptions method of AppDelegate.m before return YES;.

4. Run Grocery Cloud on a device or the iOS Simulator to generate the StackMob schema automatically. Internet connectivity must be available the first time you do this in order for the schema to be generated automatically. You can safely ignore any “Object will be placed in unnamed section” messages, which are explained in Appendix B.

5. Click Schema Configuration on the StackMob Dashboard to reveal the automatically generated schema. The expected result is shown in Figure 16.6.

Image

Figure 16.6 Automatically generated schema

6. Comment out [self generateStackMobSchema]; in the didFinishLaunchingWithOptions method AppDelegate.m.

Schema Permissions

By default, StackMob schema permissions are open. This means that objects can be Created, Read, Updated, and Deleted (CRUD) by anyone. Although this is useful during development, it is not ideal for Grocery Cloud in production. The intention of Grocery Cloud is to facilitate a common shopping list between a two or more people. To achieve this, user accounts are required to restrict who can do what. If two people need to share a shopping list, they will need to authenticate using the same StackMob user account. As far as the end user is concerned, he or she will actually know of a “user” as a “shared list.” A “shared list” will, of course, just be a user account requiring a password.

Update the grocery_cloud schemas as follows to limit access:

1. Enter Schema Configuration of the StackMob Dashboard.

2. Edit the item schema.

3. Scroll down to the Schema Permissions section and configure the permission levels shown in Figure 16.7.

Image

Figure 16.7 Schema permission levels

4. Click Save Schema.

5. Click Schema Configuration to return to the list of schemas.

6. Repeat steps 2 to 5 for the item_photolocationathomelocationatshop, and unit schemas.

With permission levels now in place, user accounts are required to work with data. As such, a new interface will be added that allows users to create a “shared list” (user account). Any objects that have been created up until now will still have open permissions. If you try to create or edit an object, access will be denied and context saves will fail.

Authentication

The permissions structure has been put in place to allow people to read, update, or delete only what they have created. This means that different people can maintain their own list without their items being visible to others. Because the concept of a “user” and a “shared list” is the same, if you want to share a list, you just tell someone the shared list name and password. To support this, a new interface will be implemented to allow a shared list to be created or logged in to.

Update Grocery Cloud as follows to implement a More tab for list sharing:

1. Select Main.storyboard.

2. Drag a View Controller on to the storyboard beneath Navigation Controller – Shop.

3. Select the new View Controller and click Editor > Embed In > Navigation Controller.

4. Hold Control and drag a line from the center of the Tab Bar Controller to the new Navigation Controller; then select Relationship Segue > view controllers.

5. Set the Identifier of the new Tab Bar Item to More using Attributes Inspector (Option+image+4).

6. Set the Navigation Item Title of the new View Controller to Shared Lists. Figure 16.8 shows the expected results.

Image

Figure 16.8 Shared lists LoginVC

From now on, the new view will be referred to as the LoginVC view, as this will be the name of a new custom class created specifically for it. This view will first be updated with new interface elements so the user can authenticate and create a shared list “account.”

Update Grocery Cloud as follows to update the LoginVC view:

1. Drag two Text Fields, two Buttons, and one Label anywhere on to the LoginVC view.

2. Configure both Text Fields as follows using Attributes Inspector (Option+image+4):

image Set Alignment to Center.

image Set Border Style to Line (represented by a rectangle with a solid border).

3. Set the placeholder text of one of the text fields to Shared List Name and the other to Password.

4. Set the Password text field to Secure by ticking Secure.


Tip

Secure is located beneath the keyboard.


5. Set the Height of both text fields to 44 using Size Inspector (Option+image+5).

6. Set the Width of both buttons to 120.

7. Set the text of one button to Create and the other to Enter using Attributes Inspector (Option+image+4).

8. Set the Alignment of the label to Center.

9. Set the text of the label to Create or Enter a Shared List.

10. Arrange the view as shown in Figure 16.9. Widen the text fields and label to the edge guides in the process.

Image

Figure 16.9 An updated shared lists LoginVC

11. Select the Shared Lists view and click Editor > Resolve Auto Layout Issues > Reset to Suggested Constraints in View Controller. If this does not resolve the auto-layout issues, you may have to manually configure the constraints as per the final project available at the end of the chapter.

Securing the User Class

When a user authenticates, his or her password would usually be transmitted in clear text. To prevent this, customizations are required to the User NSManagedObject subclass. These customizations ensure that the sensitive traffic is encrypted so long as User objects are created with theinitIntoManagedObjectContext method shown in Listing 16.7.

Listing 16.7 User.h


#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
#import "StackMob.h"
@interface User : SMUserManagedObject
@property (nonatomic, retain) NSString * username;
@property (nonatomic, retain) NSDate * createddate;
@property (nonatomic, retain) NSDate * lastmoddate;
- (id)initNewUserInContext:(NSManagedObjectContext *)context;
@end


The code from Listing 16.7 was provided by StackMob in their “Creating a User Object” tutorial, which is available by searching their website. The implementation of this class is shown in Listing 16.8.

Listing 16.8 User.m


#import "User.h"
@implementation User
@dynamic createddate;
@dynamic lastmoddate;
@dynamic username;

- (id)initNewUserInContext:(NSManagedObjectContext *)context {
    self = [super initWithEntityName:@"User"
      insertIntoManagedObjectContext:context];
    return self;
}
@end


Update Grocery Cloud as follows to ensure that user object creation is secure:

1. Replace the contents of User.h with the code from Listing 16.7.

2. Replace the contents of User.m with the code from Listing 16.8.


Note

If NSManagedObject subclasses are regenerated, the custom User class code will be lost. Any time you need to regenerate these files, remember to update this class with the code from Listing 16.7 and Listing 16.8.


Introducing LoginVC

The next step is to add the code that will drive the Shared List view. As mentioned previously, the custom class that will drive this view is called LoginVC. This class will handle the creation and authentication of a shared list (that is, user account). The header code involved is shown in Listing 16.9.

Listing 16.9 LoginVC.h


#import <UIKit/UIKit.h>
@interface LoginVC : UIViewController <UITextFieldDelegate>

@property (strong, nonatomic) IBOutlet UITextField *usernameTextField;
@property (strong, nonatomic) IBOutlet UITextField *passwordTextField;
@property (strong, nonatomic) IBOutlet UILabel *statusLabel;
@property (strong, nonatomic) UIActivityIndicatorView *activityIndicatorView;
@property (strong, nonatomic) UIView *activityIndicatorBackground;

- (IBAction)create:(id)sender;
- (IBAction)authenticate:(id)sender;
@end


The LoginVC view will have a username (shared list name) and password text field. It will also be a UITextField delegate, so the keyboard can be dismissed when the user is finished with either text field. In addition, there will be a label for showing status and an activity view used to show what is happening. The create and authenticate methods will be used to create a new account or sign in to an existing account.

Update Grocery Cloud as follows to create the LoginVC class:

1. Select the Grocery Cloud View Controllers group.

2. Click File > New > File....

3. Click iOS > Cocoa Touch > Objective-C class and then click Next.

4. Set Subclass of to UIViewController.

5. Set the class name to LoginVC.

6. Click Next and then ensure the Grocery Cloud target is selected before clicking Create.

7. Replace the contents of LoginVC.h with the code from Listing 16.9. You may be warned that the create and authenticate methods aren’t implemented yet if you click LoginVC.m.

8. Select Main.storyboard.

9. Ensure the Shared Lists View Controller is selected and then set its custom class to LoginVC using Identity Inspector (Option+image+3), as shown in Figure 16.10.

Image

Figure 16.10 Custom class: LoginVC

The next step is to link each text field, button, and label to the appropriate properties in the LoginVC header. This will allow them to be referenced by the implementation of LoginVC.

Update Grocery Cloud as follows to link the new user interface elements to code:

1. Double-click LoginVC.h so that it opens in a new window.

2. Select Main.storyboard and position LoginVC.h near the Shared Lists View Controller.

3. Hold down Control and drag a line from the Shared List Name text field to the usernameTextField property in LoginVC.h.

4. Hold down Control and drag a line from the Password text field to the passwordTextField property in LoginVC.h.

5. Hold down Control and drag a line from the Label to the statusLabel property in LoginVC.h.

6. Hold down Control and drag a line from the Create button to the create method in LoginVC.h.

7. Hold down Control and drag a line from the Enter button to the authenticate method in LoginVC.h.

Now that the interface is linked to the code, LoginVC can be implemented. The starting code required in LoginVC.m is shown in Listing 16.10.

Listing 16.10 LoginVC.m


#import "LoginVC.h"
#import "CoreDataHelper.h"
#import "AppDelegate.h"
#import "User.h"

@implementation LoginVC
#define debug 1

#pragma mark - VIEW
- (void)updateStatus {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    CoreDataHelper *cdh =
    [(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];

    if([cdh.stackMobClient isLoggedIn]) {

        [cdh.stackMobClient getLoggedInUserOnSuccess:^(NSDictionary *result) {
            self.statusLabel.text =
           [NSString stringWithFormat:@"You're using '%@'",
                                            [result objectForKey:@"username"]];
        } onFailure:^(NSError *error) {
            self.statusLabel.text = @"Create or Enter a Shared List";
        }];

    } else {
        self.statusLabel.text = @"Create or Enter a Shared List";
    }
}
- (void)viewDidLoad {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    [super viewDidLoad];
    [_usernameTextField setDelegate:self];
    [_passwordTextField setDelegate:self];
    [self hideKeyboardWhenBackgroundIsTapped];
    [self updateStatus];
}

#pragma mark - WAITING
- (void)showWait:(BOOL)visible {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    if (!_activityIndicatorBackground) {
        _activityIndicatorBackground =
        [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];

    }
    [_activityIndicatorBackground
                 setCenter:CGPointMake(self.view.frame.size.width/2,
                                       self.view.frame.size.height/2)];
    [_activityIndicatorBackground setBackgroundColor:[UIColor blackColor]];
    [_activityIndicatorBackground setAlpha:0.5];
    _activityIndicatorView = [[UIActivityIndicatorView alloc]
               initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
    _activityIndicatorView.center =
                   CGPointMake(_activityIndicatorBackground.frame.size.width/2,
                               _activityIndicatorBackground.frame.size.height/2);

    if (visible) {
        [self.view addSubview:_activityIndicatorBackground];
        [_activityIndicatorBackground addSubview:_activityIndicatorView];
        [_activityIndicatorView startAnimating];

    } else {
        [_activityIndicatorView stopAnimating];
        [_activityIndicatorView removeFromSuperview];
        [_activityIndicatorBackground removeFromSuperview];
    }
}

#pragma mark - ALERTING
- (void)showAlertWithTitle:(NSString*)title message:(NSString*)message {

if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:title
                                                    message:message
                                                   delegate:nil
                                          cancelButtonTitle:nil
                                          otherButtonTitles:@"Ok", nil];
    [alert show];
    [self showWait:NO];
}

#pragma mark - VALIDATION
- (BOOL)textFieldIsBlank {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    if ([_usernameTextField.text isEqualToString:@""] ||
        [_passwordTextField.text isEqualToString:@""]) {

        [self showAlertWithTitle:@"Please Enter a Shared List Name and Password"
                         message:@"If you don't have a Shared List you can create one by filling in a Shared List Name and a Password, then clicking Create"];
        return YES;
    }
    return NO;
}

#pragma mark - DELEGATE: UITextField
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    [textField resignFirstResponder];
    return YES;
}

#pragma mark - INTERACTION
- (void)hideKeyboardWhenBackgroundIsTapped {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    UITapGestureRecognizer *tgr =
    [[UITapGestureRecognizer alloc] initWithTarget:self
                                            action:@selector(hideKeyboard)];
    [tgr setCancelsTouchesInView:NO];
    [self.view addGestureRecognizer:tgr];
}
- (void)hideKeyboard {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    [self.view endEditing:TRUE];
}
@end


The new code is initially broken into six sections:

image The VIEW section contains methods used to update the status label with the status of the shared list and configure the initial view.

image The WAITING section contains a method used to toggle an activity indicator in cases where network calls are necessary.

image The ALERTING section contains a method used to wrap a standard UIAlertView. This simply reduces the amount of code required because there will be quite a few calls to create alert views in the future.

image The VALIDATION section is used to alert the user when either text field is blank and he or she tries to create or authenticate to a shared list.

image The DELEGATE: UITextField section contains a method that is used to hide the keyboard when either text field loses focus.

image The INTERACTION section contains two methods that are used to hide the keyboard when the background is touched.

Update Grocery Cloud as follows to begin the implementation of LoginVC:

1. Replace all code in LoginVC.m with the code from Listing 16.10.

Two additional methods are required in LoginVC.m that are already mentioned in LoginVC.h. Their absence will currently be causing an incomplete implementation warning. Listing 16.11 shows the implementation of the create method, which is the first of the two methods to be placed in a new ACCOUNT section of LoginVC.m.

Listing 16.11 LoginVC.m: create


#pragma mark - ACCOUNT
- (IBAction)create:(id)sender {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    CoreDataHelper *cdh =
    [(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
    NSManagedObjectContext *stackMobContext =
    [cdh.stackMobStore contextForCurrentThread];

    [self showWait:YES];
    if ([self textFieldIsBlank]) {
        return;
    }

    // ENSURE NETWORK IS REACHABLE
    if (!cdh.stackMobClient.networkMonitor.currentNetworkStatus ==
                                           SMNetworkStatusReachable) {

        [self showAlertWithTitle:@"Failed to Create Shared List"
                      message:@"The Internet connection appears to be offline."];
        [self updateStatus];
        return;
    }

    // ENSURE USER DOESN'T EXIST
    NSFetchRequest *fetchRequest =
    [[NSFetchRequest alloc] initWithEntityName:@"User"];
    [fetchRequest setPredicate:[NSPredicate predicateWithFormat:@"username==%@",
                                                  self.usernameTextField.text]];

    [stackMobContext executeFetchRequest:fetchRequest
                               onSuccess:^(NSArray *results) {

        if ([results count] == 1) {
            // USER ALREADY EXISTS
            [self showAlertWithTitle:@"Please choose another Shared List Name"
                             message:[NSString stringWithFormat:@"Someone has already created a list with the name '%@'",_usernameTextField.text]];
        } else {

            // CREATE USER
            self.statusLabel.text =
            [NSString stringWithFormat:@"Creating Shared List '%@'...",
                                              _usernameTextField.text];
            User *newUser =
            [[User alloc] initNewUserInContext:stackMobContext];
            [newUser setUsername:_usernameTextField.text];
            [newUser setPassword:_passwordTextField.text];

            [stackMobContext saveOnSuccess:^{

                // USER CREATED SUCCESSFULLY
                [self updateStatus];
                [self showWait:NO];
                [self authenticate:self];

            } onFailure:^(NSError *error) {

                // USER CREATION FAILED
                [stackMobContext deleteObject:newUser];
                [newUser removePassword];
                [self updateStatus];
                [self showWait:NO];
                [self showAlertWithTitle:@"Failed to Create Shared List"
                                message:[NSString stringWithFormat:@"%@",error]];
            }];
        }
    } onFailure:^(NSError *error) {

        // UNSURE IF USER EXISTS
        [self showAlertWithTitle:@"Failed to Check if Shared List Exists"
                         message:[NSString stringWithFormat:@"%@",error]];
    }];
}


The commenting in Listing 16.11 should go a long way toward explaining what is happening at each step in the code. First, the activity indicator is displayed as the account creation begins. The method will return prematurely if the text fields are blank or the network is unavailable. A check is then performed to see if the user already exists; if not, a new user is created. If the existing user check or user creation fails, the user will be notified and the method will return. If the user creation succeeds, the user will be notified and the authenticate method will be triggered.

Update Grocery Cloud as follows to implement the create method:

1. Add the code from Listing 16.11 to the top of LoginVC.m, just before the VIEW section.

The final method required in LoginVC.m is called authenticate. Listing 16.12 shows the code involved in this new method.

Listing 16.12 LoginVC.m: authenticate


- (IBAction)authenticate:(id)sender {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    if ([self textFieldIsBlank]) {
        return;
    }
    CoreDataHelper *cdh =
    [(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];

    self.statusLabel.text =
    [NSString stringWithFormat:@"Connecting to Shared List '%@'...",
                                           _usernameTextField.text];
    [self showWait:YES];

    // ensure new objects are saved prior to an account switch
    [[cdh.stackMobStore contextForCurrentThread] saveOnSuccess:^{

        [cdh.stackMobClient loginWithUsername:_usernameTextField.text
                                     password:_passwordTextField.text
                                    onSuccess:^(NSDictionary *results) {

            [self showAlertWithTitle:@"Success!"
                             message:[NSString stringWithFormat:
   @"You're now using Shared List '%@'", [results valueForKey:@"username"]]];
            [self updateStatus];
            [self showWait:NO];

            [[NSNotificationCenter defaultCenter]
                                   postNotificationName:@"SomethingChanged"
                                                 object:nil
                                               userInfo:nil];
        } onFailure:^(NSError *error) {

            if (error.code == 401) {
                [self showAlertWithTitle:@"Failed to Enter Shared List"
                                 message:@"Access Denied"];
            } else {
                [self showAlertWithTitle:@"Failed to Enter Shared List"
                                 message:[NSString stringWithFormat:@"%@",
                                             error.localizedDescription]];
            }
            [self updateStatus];
            [self showWait:NO];
        }];
    } onFailure:^(NSError *error) {
        NSLog(@"Failed to save context prior to account switch");
    }];
}


The authenticate method displays an activity indicator and updates the status label prior to commencing work. It also exits prematurely if either text field is empty. The context is saved prior to a login attempt just in case there is data from an old list that is yet to be persisted. Provided this save succeeds, the StackMob client method loginWithUsername method is called. The most common failed response will be 401, which means access has been denied. This error is specifically handled in the login failure code. Should authentication be successful, the status is updated, the activity indicator is hidden, and a SomethingChanged notification is sent so that the table views are refreshed.

Update Grocery Cloud as follows to implement the create method:

1. Add the code from Listing 16.12 to the bottom of the ACCOUNT section of LoginVC.m, just before the VIEW section.

2. Delete Grocery Cloud from your device, click Product > Clean, and then run the application to reinstall it.

3. Select the More tab and create a Shared List by entering a list name and password; then clicking Create. The expected result is shown in Figure 16.11.

Image

Figure 16.11 Successful creation of a shared list (that is, user)

4. Return to the Prepare tab and create a new item. As you create the item, also create a new home and shop location. Once you’ve done that, assign the new locations to the item using the picker views. If your new item does not show up, try switching tabs to trigger a refresh.

Maintaining Responsiveness

Even with caching enabled, it is inevitable that calls to the network will be required at some point. In LoginVC, an activity indicator is displayed whenever a network call is required using showWait. A similar approach needs to be implemented for when each table view performs a fetch. The code in the WAITING and ALERTING sections of LoginVC will be reused and the performFetch method updated in CoreDataTVC.m in order for this to be achieved. At the same time, a UIRefreshControl will be implemented that enables the user to manually call performFetch by swiping down on anyCoreDataTVC-driven table view.

The updated performFetch method is shown in Listing 16.13.

Listing 16.13 CoreDataTVC.m: performFetch


- (void)performFetch {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    if (self.refreshControl.refreshing) {
        [self.refreshControl endRefreshing];
    } else {
        [self showWait:YES];
    }
    if (self.frc) {
        [self.frc.managedObjectContext performBlock:^{

            NSError *error = nil;
            if (![self.frc performFetch:&error]) {

                NSLog(@"%@ '%@' %@",self.class, NSStringFromSelector(_cmd),
                                                                    error);
               if ([error.domain isEqualToString:@"HTTP"] && error.code == 401) {

                [self showAlertWithTitle:@"Access Denied"
                message:@"Please Create or Enter a Shared List on the More tab"];
               } else {
                   if (error) {
                        [self showAlertWithTitle:@"Fetch Failed"
                                       message:[NSString stringWithFormat:@"%@",
                                                   error.localizedDescription]];
                    }
                }
            }
            [self.tableView reloadData];
            [self showWait:NO];
        }];
    } else {
       NSLog(@"Failed to perform fetch: The fetched results controller is nil.");
    }
}


Update Grocery Cloud as follows to ensure table views show an activity indicator:

1. Copy the activityIndicatorView and activityIndicatorBackground properties from LoginVC.h to CoreDataTVC.h, placing them after the existing properties.

2. Copy the WAITING and ALERTING sections from LoginVC.m to CoreDataTVC.m, placing them just before the existing FETCHING section. These sections contain the showWait and showAlertWithTitle methods.

3. Replace the performFetch method in CoreDataTVC.m with the method from Listing 16.13.

4. Add the code shown in Listing 16.14 to CoreDataTVC.m on the line above the FETCHING section.

5. Run the application again to test that the activity indicator is displayed while table view fetches are performed.

Listing 16.14 CoreDataTVC.m: viewDidLoad


#pragma mark - VIEW
- (void)viewDidLoad {
    [super viewDidLoad];
    UIRefreshControl *refreshControl = [UIRefreshControl new];
    [refreshControl addTarget:self
                       action:@selector(performFetch)
             forControlEvents:UIControlEventValueChanged];
    [self setRefreshControl:refreshControl];
}


Summary

Congratulations, you’ve now configured a backend service and have integrated it with an existing Core Data application! There are a still few areas requiring additional refinement that are excluded from this chapter to keep it as succinct as possible. For example, the transition fromPrepareTVC to ItemVC still lags when a network call is made. The code also needs to be refactored to display an activity indicator while items are inserted or retrieved from the network. Still, you should now be in a good position to apply the StackMob framework to your own applications.

If you need photo (binary data) support, you’ll need an Amazon S3 “bucket.” To create an S3 bucket, you’ll first need to sign up for an Amazon Web Services (AWS) account at http://aws.amazon.com/. As you create an Amazon AWS account, you’ll need to provide a valid credit card number. Pricing can be viewed at http://aws.amazon.com/pricing/s3/ and varies based on the region the bucket is placed in. Search the StackMob website for the “Upload to S3” tutorial for further information.

Exercises

Why not build on what you’ve learned by experimenting?

1. Examine the StackMob contents by visiting Schema Configuration and clicking View Data for the item schema.

2. Create a new Shared List and populate it with some new items. Again, view the data on the Schema Configuration page for the item schema. Notice how the sm_owner schema field indicates what shared list (that is, user) owns and can therefore read, update, or delete each row.

3. Enable StackMob debug by setting SM_CORE_DATA_DEBUG to YES in the init method of CoreDataHelper.m. Run the application again and examine the console log. You should see a lot of verbose logging giving greater visibility of what’s happening under the hood.

For your convenience, the final Grocery Cloud project is available for download from http://timroadley.com/LearningCoreData/GroceryCloud-AfterChapter16.zip.