Aisle411 iOS MapSDK (Obj-C)

Introduction to Aisle411 iOS MapSDK (Obj-C)

The walkthrough on this page is for Objective-C. If you would like to see the Swift 3/4 walkthrough, please see this page.

The Aisle411 iOS MapSDK allows for the a developer to user all of the tools provided by Aisle411's product and user location technologies. Below you will find a tutorial, along with several XCode Projects that will help ease you into using Aisle411's tools to display a store map, perform searches for both single and shopping lists of products, as well as display user location and route the user to his or her destination within the store.

If you would like to follow along and move along with the walkthrough, proceed with the next steps. If you would like to download the demo in it's entirety, and simply read along with the writeup here, you may find the project files here.

Phase One - Initial Setup

Download MapSDK Files

The first thing that we need to do is to download the files for the Aisle411 MapSDK for iOS. You will need to select one of the following two versions:

Create new iOS project

Select Single View Application. Name your project accordingly on the following screen. The images below may be of aid to you.

Screenshot Screenshot

Add Linked Frameworks and Libraries

Click on app project file. Go down to Linked Frameworks and Libraries in the General tab. The images below may be of aid to you.

Screenshot Screenshot

Click the + sign and add the following files:

  • libz.tbd
  • libstdc++.6.0.9.tbd
  • CoreLocation.framework
Screenshot

The MapSDK is a framework too! Find the location that you saved the MapSDK and drag it into the Frameworks folder in XCode. You should see a popup that looks like the following image. Make sure that "Copy items if needed" and "Create groups" are selected.

Screenshot

Overhead Components

The following two sections will cover all "overhead" components that will be used in the ViewControllers for this demo. AMDGoButton and AMDGetRequester take care of lots of the heavy-lifting in generating web service HTTP calls and parsing the results. These components will be included all of the ViewControllers for this demo!

Intro + AMDGoButton.h

The AMDGoButton handles executing a request with the given fields filled, and takes us to our results page. The simple header file is as follows:


// AMDGoButton.h

#import <UIKit/UIKit.h>

@interface AMDGoButton : UIButton

@end
                    

AMDGoButton.m

The implementation for AMDGoButton is as follows:


// AMDGoButton.m 

#import "AMDGoButton.h"

@implementation AMDGoButton

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code
        self.layer.cornerRadius = 10;
        self.clipsToBounds = YES;
        self.backgroundColor = [UIColor colorWithRed:0.2 green:0.8 blue:0.2 alpha:1];
        
    }
    return self;
}

/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
    // Drawing code
}
*/

@end
                    

The implementation of this class is mainly aesthetic, and provides a commented out section with framework for you to further alter visual aspects.

Intro + AMDGetRequester.h

The AMDGetRequester component handles all of the hard work in sending HTTP requests to Aisle411's web services. This file class handles partner IDs, partner secrets, and authentication as well, in order to make the job of the actual ViewControllers both easier to implement and easier to read.


// AMDGetRequester.h

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>


@interface AMDGetRequester : NSObject{
    
    /****************************************
     The Following Information Must be Entered in the UserInfo.plist in Supporting Files
     ****************************************/
    NSString *partnerID;//partnerIndentifier in UserInfo.plist
    NSString *partnerSecret;//partnerSecret in UserInfo.plist
    NSString *baseURL;//Pre entered, testing server
}

@property (weak, nonatomic) UIViewController *ownerVC;

-(NSString*)SendGetRequest:(NSString*)method withParameters:(NSArray*)parameters;
- (void)sendWithPostMethodAndCompletionHandler: (NSMutableDictionary *)arguments block:(void(^)(NSDictionary *resultDictionary)) completeBlock;
-(NSString *)deviceToken;

@end
                    

As described above, the partnerID and partnerSecret need to be entered into UserInfo.plist in order to function properly. These are initially filled with our sample developer ID and secret. If you are looking to do more work with our web services, reach out to us at: tech (at) aisle411 (dot) com, with an inquiry about obtaining a personalize partnerID and partnerSecret.

The ownerVC property is maintained in order to allow the parent ViewController to send messages and alerts based on the HTTP response we receive from web service calls.

SendGetRequest and sendWithPostMethodAndCompletionHandler are the two main functions in this class. They actually form the proper HTTP request based on parameters given, then help to handle the response received from the call.

AMDGetRequester.m

The implementation of AMDGetRequester is as follows:


// AMDGetRequester.m

#import "AMDGetRequester.h"
#import <CommonCrypto/CommonDigest.h>

@implementation AMDGetRequester

/**************************************************
 Initialization is to find all User Information
 If user Information was not entered, it will throw exceptions in SendGetRequest Method
 *************************************************/
-(id) init{
    
    NSString* userInfoPath = [[NSBundle mainBundle] pathForResource:@"UserInfo" ofType:@"plist"];
    NSDictionary* userInfoDict = [[NSDictionary alloc] initWithContentsOfFile:userInfoPath];
    
    partnerID = userInfoDict[@"partnerIdentifier"];
    partnerSecret = userInfoDict[@"partnerSecret"];
    baseURL = userInfoDict[@"baseURL"];
    
    return self;
}

/*****************
 SendGetRequest will return an NSString containing the GET request URL
 *****************/

-(NSString*)SendGetRequest:(NSString*)method withParameters:(NSArray*)parameters{
    if ([partnerID length] == 0) {
        @throw [NSException exceptionWithName:@"Empty partner ID" reason:@"partnerIdentifier in UserInfo.plist must be specified" userInfo:nil];
    }
    if ([partnerSecret length] == 0) {
        @throw [NSException exceptionWithName:@"Empty partner Secret" reason:@"partnerSecret in UserInfo.plist must be specified" userInfo:nil];
    }
    NSString* url = [NSString stringWithFormat:@"%@/%@", baseURL, [self AddAuth:method withParameters:[parameters arrayByAddingObject:[NSString stringWithFormat:@"partner_id=%@", partnerID]]]];
    
    //Escape Spaces with '%20'
    url = [url stringByAddingPercentEncodingWithAllowedCharacters:(NSCharacterSet.URLQueryAllowedCharacterSet)];
    NSLog(@"%@", url);
    
    return url;
}

/****************
 Using MD5 to add Authentication
 ***************/
-(NSString*)AddAuth:(NSString*)method withParameters:(NSArray*)parameters{
    
    //Step 1: Sort parameters
    NSArray* sortedResult = [parameters sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
    
    //Step 2: Append sorted parameters one by one to the end of the request
    NSString* toHash = [method stringByAppendingString:@"?"];
    
    NSString* result = [method stringByAppendingString:@".php?"];
    
    for (int i = 0; i < [sortedResult count]; i++) {
        if (i > 0) {
            toHash = [toHash stringByAppendingString:@"&"];
            result = [result stringByAppendingString:@"&"];
        }
        toHash = [toHash stringByAppendingString:sortedResult[i]];
        result = [result stringByAppendingString:sortedResult[i]];
    }
    
    //Step 3: Add Partner Secret
    toHash = [toHash stringByAppendingString:[NSString stringWithFormat:@"&%@", partnerSecret]];
    
    
    //Step 4: Encrypt with MD5
    const char *cStr = [toHash UTF8String];
    
    unsigned char digest[CC_MD5_DIGEST_LENGTH];
    
    CC_MD5(cStr, (CC_LONG)strlen(cStr), digest);
    
    NSMutableString* hashed = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH *2];
    
    for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
        [hashed appendFormat:@"%02x", digest[i]];
    }
    
    //Step 5: Append generated authentication to the end of the request
    result = [[result stringByAppendingString:@"&auth="] stringByAppendingString:hashed];
    
    
    return result;
}

#pragma mark - device token related
static NSString *guid()
{
    if ([[NSUserDefaults standardUserDefaults] stringForKey:@"GUID"])
    {
        return [[NSUserDefaults standardUserDefaults] stringForKey:@"GUID"];
    }
    else
    {
        CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
        NSString *uuidString = (NSString *)CFBridgingRelease(CFUUIDCreateString(kCFAllocatorDefault, uuid));
        CFRelease(uuid);
        [[NSUserDefaults standardUserDefaults]
         setObject:uuidString forKey:@"GUID"];
        return uuidString;
    }
}

static NSString *sha1(NSString *string)
{
    NSData *dataToHash = [string dataUsingEncoding:NSUTF8StringEncoding];
    
    unsigned char hashBytes[CC_SHA1_DIGEST_LENGTH];
    CC_SHA1([dataToHash bytes], (unsigned int)[dataToHash length], hashBytes);
    
    
    char hashString[2 * CC_SHA1_DIGEST_LENGTH + 1] = {0};
    int i = 0;
    for (i = 0; i < CC_SHA1_DIGEST_LENGTH; i++)
    {
        snprintf(&hashString[i * 2], 3, "%02x", hashBytes[i]);
    }
    
    return [NSString stringWithUTF8String:hashString];
}

-(NSString *)deviceToken
{
    return sha1(guid());
}

static NSString *md5(NSString* string)
{
    NSData *dataToHash = [string dataUsingEncoding:NSUTF8StringEncoding];
    
    unsigned char hashBytes[CC_MD5_DIGEST_LENGTH];
    CC_MD5([dataToHash bytes], (unsigned int)[dataToHash length], hashBytes);
    
    char hashString[2 * CC_MD5_DIGEST_LENGTH + 1] = {0};
    int i = 0;
    for (i = 0; i < CC_MD5_DIGEST_LENGTH; i++)
    {
        snprintf(&hashString[i * 2], 3, "%02x", hashBytes[i]);
    }
    
    return [NSString stringWithUTF8String:hashString];
}

#pragma mark -post method
- (void)sendWithPostMethodAndCompletionHandler: (NSMutableDictionary *)arguments block:(void(^)(NSDictionary *resultDictionary)) completeBlock
{
    NSString *url = @"https://aisle411.ws/webservices3/locateitems.php";
    
    //create request
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:url]];
    
    //create data from pass-in arguments
    NSError *error = nil;
    NSData *data = [NSJSONSerialization dataWithJSONObject:arguments options:0 error:&error];
    
    //encoded body
    NSString *body = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    
    //remeber the method is post
    [request setHTTPMethod:@"POST"];
    
    //note here, it must use md5 for authentication
    //set http header field "Authentication" with json string + secret
    [request setValue:md5([body stringByAppendingString:partnerSecret]) forHTTPHeaderField:@"Authentication"];
    
    //set http body with json data
    [request setHTTPBody:data];
    NSURLSession *session = [NSURLSession sharedSession];
    
    [[session dataTaskWithRequest: request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        NSDictionary *resultDict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error];
        
        if (data == nil) {
            UIAlertController *alert = [UIAlertController
                                        alertControllerWithTitle:@"Error"
                                        message:@"No Data Was Received"
                                        preferredStyle:UIAlertControllerStyleAlert];
            UIAlertAction *confirm = [UIAlertAction
                                      actionWithTitle:@"Confirm"
                                      style:UIAlertActionStyleDefault
                                      handler: nil];
            [alert addAction: confirm];
            [self.ownerVC presentViewController:alert animated:YES completion:nil];
            return;
        }
        else if (error) {
            UIAlertController *alert = [UIAlertController
                                        alertControllerWithTitle:@"Error"
                                        message:@"There was an error processing the result."
                                        preferredStyle:UIAlertControllerStyleAlert];
            UIAlertAction *confirm = [UIAlertAction
                                      actionWithTitle:@"Confirm"
                                      style:UIAlertActionStyleDefault
                                      handler: nil];
            [alert addAction: confirm];
            [self.ownerVC presentViewController:alert animated:YES completion:nil];
        }
        else{
            if (completeBlock) completeBlock(resultDict);
        }
    }] resume];;
}

@end
                    

As you can see above, this implementation covers the entire formation of the GET or POST request, including authentication and hashing measures. The code above is fairly well commented if you'd like to examine it more closely. With that, we are finished looking at the overhead components to this demo. Now we can dive into the specific use cases of our web services in your application.

Use Cases as View Controllers

For this application, lots of the design choices are arbitrary, with a few must-haves. Therefore, I will not spend time describing how to build the storyboard, what layout to use, etc. but will instead link to Storyboard files to download as we progress through the demo. I will be using a Tab Bar Controller as the base for each use-case that we will explore. This makes it easy to ignore the use-cases that you may not care about, and to focus on one use-case at a time. If you would like to remove certain use-cases, feel free to remove the relevant portions from the Storyboard. Feel free to make any changes you deem fit to the Storyboard files. The final download will have all use-cases set-up, and I'd recommend if you're going to make changes, that you make them beginning from that version, as to not accidentally overwrite any customizations.

Each of the following ViewControllers will represent a unique use-case, and none of the code from any of the controllers is required for another to function. Therefore, if you'd like to skip around to a use-case that you are particularly interested in, feel free to do so.

Intro + AMDStoreMapViewController.h

The AMDStoreMapViewController will allow you to search for a store via a retailer_store_id in a search bar, then press a Go button to perform the web service call to grab that map and display it. The header file is as follows:


// AMDStoreMapViewController.h

#import <UIKit/UIKit.h>

@interface AMDStoreMapViewController : UIViewController

@property (strong, nonatomic) IBOutlet UITextField *storeIdArea;

@property (strong, nonatomic) IBOutlet UIButton *goButton;

@property (strong, nonatomic) IBOutlet UIView *resultArea;
@property (strong, nonatomic) IBOutlet UILabel *messageArea;

@end
                    

These properties will make more sense with the accompanying implementation to follow.

AMDStoreMapViewController.m

Here's the implementation of AMDStoreMapViewController:


// AMDStoreMapViewController.m

#import "AMDStoreMapViewController.h"

#import "AMDGetRequester.h"

#import "MapBundle.h"
#import "MapController.h"
#import "MapBundleParser.h"
#import <MapKit/MapKit.h>

@interface AMDStoreMapViewController ()

@end

@implementation AMDStoreMapViewController{
    NSMutableArray* jsonArray;//To store returned JSON
    MapBundle* mapBundle;
    MapController* mapController;
    AMDGetRequester* getRequester;
    NSString* path;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    getRequester = [[AMDGetRequester alloc] init];
    getRequester.ownerVC = self;
    
    //Setup map area
    mapController = [[MapController alloc] init];
    mapController.view.frame = _resultArea.frame;
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (void)showMap{
    // To parse the fetched map data
    MapBundleParser *parser = [[MapBundleParser alloc] initWithPathToArchive:path];
    mapBundle = [parser parse];
    
    //initialize the mapController, set its view to desired view
    mapController = [[MapController alloc] init];
    mapController.mapBundle = mapBundle;
    mapController.view.frame = _resultArea.bounds;
    [_resultArea addSubview:mapController.view];
    
    [mapController setFloor:1];
    [mapController compassAction:nil];
    [mapController setCompassEnabled:NO];
    mapController.view.backgroundColor = [UIColor whiteColor];
    [mapController setLogoBottomOffset:0];
}

- (IBAction)goButtonPressed:(id)sender {
    _messageArea.textColor = [UIColor blackColor];
    _messageArea.text = @"Loading...";
    
    //Hide Keyboard
    [_storeIdArea endEditing:YES];
    
    
    if ([_storeIdArea.text isEqualToString:@""]) {
        _messageArea.textColor = [UIColor redColor];
        _messageArea.text = @"Please enter a valid Store ID";
        return;
    }
    
    else{
        // The parameters for the getRequester
        NSArray* toPassIn = [NSArray arrayWithObject:[NSString stringWithFormat:@"retailer_store_id=%@", _storeIdArea.text]];
        
        // Use the getRequester to get the request URL
        NSString* url = [getRequester SendGetRequest:@"map" withParameters:toPassIn];
        
        dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            
            //get the last-modified time stamp from server
            __block NSString *server_timestamp;
            NSURL *realurl = [NSURL URLWithString:url];
            NSURLSession *session = [NSURLSession sharedSession];
            [[session dataTaskWithURL: realurl completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
                NSHTTPURLResponse *httpresp = (NSHTTPURLResponse *) response;
                if ([httpresp respondsToSelector:@selector(allHeaderFields)]){
                    NSDictionary *dictionary = [httpresp allHeaderFields];
                    server_timestamp = [dictionary objectForKey:@"Last-Modified"];
                    NSLog(@"server timestamp is %@",server_timestamp);
                }
            }] resume];
            
            //check the map timestamp locally
            NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
            NSString *time_stamp = [defaults objectForKey:_storeIdArea.text];
            NSLog(@"user_defalut timestamp is %@", time_stamp);
            
            //if timestamp doesn't exist or is not equal to the server
            if(![server_timestamp isEqualToString:time_stamp]){
                
                //update timestamp locally
                [defaults setObject:server_timestamp forKey:_storeIdArea.text];
                [defaults synchronize];
                NSLog(@"now user_defalut timestamp is %@",[defaults objectForKey:_storeIdArea.text]);
                
                // Background processing
                NSData* urlData = [NSData dataWithContentsOfURL:[NSURL URLWithString:url]];
                //NSLog(@"Data From Show Map Controller: %@", urlData);
                if (urlData == nil) {
                    _messageArea.textColor = [UIColor redColor];
                    _messageArea.text = @"No Data Was Received";
                    return;
                }
                
                // Update the UI/send notifications based on the
                // Results of the background processing
                dispatch_async( dispatch_get_main_queue(), ^{
                    if ([NSJSONSerialization JSONObjectWithData:urlData options:kNilOptions error:nil] != nil) {
                        NSLog(@"JSON DETECTED");
                        _messageArea.textColor = [UIColor redColor];
                        _messageArea.text = @"Map Not Found";
                        for (UIView *subview in [_resultArea subviews]) {
                            [subview removeFromSuperview];
                        }
                        return;
                    }
                    
                    _messageArea.textColor = [UIColor blackColor];
                    _messageArea.text = @"";
                    for (UIView *subview in [_resultArea subviews]) {
                        [subview removeFromSuperview];
                    }
                    
                    if (urlData) {
                        NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
                        NSString  *documentsDirectory = [paths objectAtIndex:0];
                        
                        NSString  *filePath = [NSString stringWithFormat:@"%@/%@", documentsDirectory, [NSString stringWithFormat:@"%@.imap", _storeIdArea.text]];
                        [urlData writeToFile:filePath atomically:YES];
                        
                        NSFileManager* fileMgr = [NSFileManager defaultManager];
                        path = filePath;
                        NSLog(@"new path is %@",path);
                        if ([fileMgr fileExistsAtPath:filePath]) {
                            _messageArea.textColor = [UIColor redColor];
                            _messageArea.text = [NSString stringWithFormat:@"Cache Map for ID: %@", _storeIdArea.text];
                            NSLog(@"store map locally!");
                            [self showMap];
                        }
                        
                    }else{
                        _messageArea.textColor = [UIColor redColor];
                        _messageArea.text = @"There Was No Data Received";
                    }
                });
            }
            else{
                //use cached map
                dispatch_async( dispatch_get_main_queue(), ^{
                    
                    for (UIView *subview in [_resultArea subviews]) {
                        [subview removeFromSuperview];
                    }
                    
                    NSArray *exist_paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
                    NSString  *exist_documentsDirectory = [exist_paths objectAtIndex:0];
                    
                    NSString* exist_filePath = [exist_documentsDirectory stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.imap", _storeIdArea.text] ];
                    
                    NSFileManager* fileMgr = [NSFileManager defaultManager];
                    
                    path = exist_filePath;
                    NSLog(@"the exist path is %@",path);
                    if ([fileMgr fileExistsAtPath:exist_filePath]) {
                        _messageArea.textColor = [UIColor greenColor];
                        _messageArea.text = [NSString stringWithFormat:@"Found Local Map for ID: %@", _storeIdArea.text];
                        NSLog(@"found map!");
                        [self showMap];
                    }else{
                        UIAlertController *alert = [UIAlertController
                                                    alertControllerWithTitle:@"Error"
                                                    message:@"The map is missing!"
                                                    preferredStyle:UIAlertControllerStyleAlert];
                        UIAlertAction *confirm = [UIAlertAction
                                                  actionWithTitle:@"Confirm"
                                                  style:UIAlertActionStyleDefault
                                                  handler: nil];
                        [alert addAction: confirm];
                        [self presentViewController:alert animated:YES completion:nil];
                        [defaults setObject:@"" forKey:_storeIdArea.text];
                        [defaults synchronize];
                    }
                });
            }
        });
    }
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    
    UITouch *touch = [[event allTouches] anyObject];
    if ([_storeIdArea isFirstResponder] && [touch view] != _storeIdArea) {
        [_storeIdArea resignFirstResponder];
    }
    [super touchesBegan:touches withEvent:event];
}
@end
                    

The comments in this file describe the logic going on in each function. Essentially, the capabilities of this ViewController are as follows:

  • Enter a retailer_store_id, and show a map for it
  • If this map is stored on the device already, use the local one instead of redownloading
  • If map stored locally is too old, or doesn't exist, download a new one and store it
This is all made significantly easier by the AMDGetRequester that we already implemented. Now, you will want to download this Storyboard and add it to your project. After that, you will be able to test run all of the above functionality on your device.

Intro + AMDProductListViewController.h

AMDProductListViewController leverages Aisle411's Single Product Search, which allows for a user to search for an individual item by term or UPC and see the results. This ViewController will handle the search query, and utilize AMDGetRequester to perform the request, parse the result, and display the items found to the user. The header file is as follows:


// AMDProductListViewController

#import <UIKit/UIKit.h>

@interface AMDProductListViewController : UIViewController

@property (strong, nonatomic) IBOutlet UITextField *textArea;

@property (strong, nonatomic) IBOutlet UIButton *goButton;

@property (strong, nonatomic) IBOutlet UILabel *messageArea;

@property (strong, nonatomic) IBOutlet UITableView *resultArea;
@property (strong, nonatomic) IBOutlet UITextField *termArea;
@property (strong, nonatomic) IBOutlet UITextField *storeIDtextArea;

@end
                    

Again, the above properties will be much clearer when given the context of the implementation.

AMDProductListViewController.m

Here's the implementation for AMDProductListViewController:


// AMDProductListViewController.m

#import "AMDProductListViewController.h"
#import "AMDGetRequester.h"
#import "AMDDetailedInfoViewController.h"

@interface AMDProductListViewController ()

@end

/*********************************
 In this Demo, Only GET requests to aisle411 server are used
 No header from MapSDK is necessary.
**********************************/

@implementation AMDProductListViewController{
    BOOL usingUPC;//To indicate if user entered an UPC or a term
    NSDictionary* resultDict;//To parse returned JSON into a NSDictionary
    AMDGetRequester* getRequester;
    
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    getRequester = [[AMDGetRequester alloc] init];
    getRequester.ownerVC = self;
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}


- (IBAction)goButtonPressed:(id)sender {
    // Hide Keyboard
    [_textArea endEditing:YES];
    [_storeIDtextArea endEditing:YES];
    
    // If User entered nothing, display a message instead
    //if ([_textArea.text isEqualToString:@""]) {
        
    if ([_textArea.text isEqualToString:@""]) {
        _messageArea.textColor = [UIColor redColor];
        _messageArea.text = @"Please Input a valid parameter";
        return;
    }
    
    else if ([_storeIDtextArea.text isEqualToString:@""]){
        _messageArea.textColor = [UIColor redColor];
        _messageArea.text = @"Please Type In StoreID";
        return;
    }
    
    else{
        // Indicates the search is in progress
        _messageArea.textColor = [UIColor blackColor];
        _messageArea.text = @"Please Wait...";
        
        // To determine wether the input is a number or a string
        NSScanner* scanner = [NSScanner scannerWithString:_textArea.text];
        NSInteger* integer = NULL;
        
        
        // The parameters for the getRequester
        NSArray* toPassIn;
        
        // To determine if user entered number(UPC) or string(term), retailer_store_id is fixed in this demo
        if ([scanner scanInteger:integer]) {
            toPassIn = [NSArray arrayWithObjects:[NSString stringWithFormat:@"retailer_store_id=%@", _storeIDtextArea.text], [NSString stringWithFormat:@"upc=%@", _textArea.text], @"start=0", @"end=7", nil];
            usingUPC = true;
        }else{
            toPassIn = [NSArray arrayWithObjects:[NSString stringWithFormat:@"retailer_store_id=%@", _storeIDtextArea.text], [NSString stringWithFormat:@"term=%@", _textArea.text], @"start=0", @"end=7", nil];
            usingUPC = false;
        }
        
        //Use the getRequester to get the request URL
        NSString* url = [getRequester SendGetRequest:@"searchproduct" withParameters:toPassIn];
        
        //Run the search in the background
        dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            
            // Background processing
            
            NSData* dataLoad = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:url]];
            
            if (dataLoad == nil) {
                _messageArea.textColor = [UIColor redColor];
                _messageArea.text = @"No Data Was Received";
                return;
            }
            
            dispatch_async( dispatch_get_main_queue(), ^{
                
                // Results of the background processing
                
                NSError* error;
                
                resultDict = [NSJSONSerialization JSONObjectWithData:dataLoad options:NSJSONReadingMutableContainers error:&error];
                
                
                if (error) {
                    _messageArea.textColor = [UIColor redColor];
                    _messageArea.text = @"There was an Error";
                    
                }else{
                    _messageArea.textColor = [UIColor greenColor];
                    
                    [self reloadList];
                    
                }
                
                [_resultArea reloadData];
            });
        });
    }
}


// Only for message displaying purposes, shows all possible cases.
-(void)reloadList{
    if (usingUPC) {
        if (resultDict[@"products"][@"item_nm"] != nil) {
            // If the result has item_nm value, it was successfully returned one product.
            
            _messageArea.text = [NSString stringWithFormat:@"Found product By UPC"];
            
        }else{
            // If the result has no such value, it mean it returned empty result
            
            _messageArea.textColor = [UIColor redColor];
            _messageArea.text = [NSString stringWithFormat:@"Found Nothing By UPC"];
            
        }
    }else{
        if (resultDict[@"products"] != nil) {
            
            // If the returned dictionary has key products, it means found a exact match
            // Another chance is that the returned dictionary is empty (Found null)
            
            _messageArea.text = [NSString stringWithFormat:@"Found Exact Match or Null"];
            
        }else if (resultDict[@"product_suggestions"] != nil){
            
            // If the returned dictionary has key products,
            //it means found a list of items that can be found by the term
            
            _messageArea.text = [NSString stringWithFormat:@"Found Multiple Suggestions"];
            
        }else{
            
            // Else, the server determines that the user entered a typo
            // Then the server will return a list of suggestion words
            // Returned dictionary have typo_suggestions key
            _messageArea.textColor = [UIColor orangeColor];
            _messageArea.text = @"Typo, returned suggestions";
            
        }
    }
}


-(NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section{
    if (usingUPC) {
        
        // If using upc, there are only 0 or 1 possible returns
        
        return 1;
        
    }else{
        
        //Consider all possible returns.
        
        if (resultDict[@"products"] != nil) {
            return [resultDict[@"products"] count];
        }else if (resultDict[@"product_suggestions"] != nil){
            return [resultDict[@"product_suggestions"] count];
        }else{
            return [resultDict[@"typo_suggestions"] count];
        }
    }
}


-(UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    
    UITableViewCell* cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cellID"];
    
    
    if (usingUPC) {
        if (resultDict[@"products"][@"item_nm"] == nil) {
            // If the retuened value is empty
            return cell;
        }
        cell.textLabel.text = resultDict[@"products"][@"item_nm"];
    }else{
        if (resultDict[@"products"] != nil) {
            cell.textLabel.text = resultDict[@"products"][indexPath.row][@"synonym_nm"];
        }else if (resultDict[@"product_suggestions"] != nil){
            cell.textLabel.text = resultDict[@"product_suggestions"][indexPath.row][@"synonym_nm"];
        }else{
            cell.textLabel.text = resultDict[@"typo_suggestions"][indexPath.row];
        }
    }
    
    return cell;
}

- (void)tableView:(UITableView*)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    AMDDetailedInfoViewController* info = [self.storyboard instantiateViewControllerWithIdentifier:@"infoVC"];
    
    if (usingUPC) {
        if (resultDict[@"products"][@"item_nm"] != nil) {
            info.productName = resultDict[@"products"][@"item_nm"];
            info.itemID = resultDict[@"products"][@"item_id"];
            info.upc = resultDict[@"products"][@"upc"];
            info.aisle = resultDict[@"products"][@"sections"][0][@"aisle"];
        }
    }else{
        if (resultDict[@"products"] != nil) {
            info.productName = resultDict[@"products"][indexPath.row][@"synonym_nm"];
            info.itemID = resultDict[@"products"][indexPath.row][@"synonym_id"];
            info.upc = @"unknown";
            info.aisle = resultDict[@"products"][indexPath.row][@"sections"][0][@"aisle"];
        }else if (resultDict[@"product_suggestions"] != nil){
            info.productName = resultDict[@"product_suggestions"][indexPath.row][@"synonym_nm"];
            info.itemID = resultDict[@"product_suggestions"][indexPath.row][@"synonym_id"];
            info.upc = @"unknown";
            info.aisle = resultDict[@"product_suggestions"][indexPath.row][@"sections"][0][@"aisle"];
        }else{
            info.productName = @"aisle411 Typo Suggestion";
            info.itemID = nil;
            info.upc = resultDict[@"typo_suggestions"][indexPath.row];
            info.aisle = _textArea.text;
        }
    }
    
    [[info view] setNeedsDisplay];
    [self presentViewController:info animated:YES completion:nil];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    
    UITouch *touch = [[event allTouches] anyObject];
    if ([touch view] != _textArea) {
        [_textArea endEditing:YES];
    }
    [super touchesBegan:touches withEvent:event];
}

/*
#pragma mark - Navigation

// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    // Get the new view controller using [segue destinationViewController].
    // Pass the selected object to the new view controller.
}
*/

@end
                    

Again, hopefully the comments above sufficiently describe the logic in the above functions. To recap, this ViewController gives the user the ability to search for an item by search term or UPC for a particular store, the results of which will appear in a table.

In order to get a little more information about the results that appear in this table, and in attempt to not over-complicated this ViewController, we shall add another called "AMDDetailedInfoViewController" that will connect to this ViewController, and will allow for the user to click on the result items to get more information about them.

AMDDetailedInfoViewController.h

Less time will be spent describing this controller because it acts as a helper to the previous, and doesn't do much on its own. AMDDetailedInfoViewController and AMDProductListViewController act as a unit to fulfil one use-case. The header is as follows:


// AMDDetailedInfoViewController.h

#import <UIKit/UIKit.h>

@interface AMDDetailedInfoViewController : UIViewController

@property (strong, nonatomic) IBOutlet UILabel *productNameLabel;
@property (strong, nonatomic) IBOutlet UILabel *itemIDLabel;
@property (strong, nonatomic) IBOutlet UILabel *upcLabel;
@property (strong, nonatomic) IBOutlet UILabel *aisleLabel;

@property (strong, nonatomic) IBOutlet UIButton *doneButton;

@property NSString* productName;
@property NSString* itemID;
@property NSString* upc;
@property NSString* aisle;

@end
                    

Let's jump right into the implementation:


// AMDDetailedInfoViewController.m

#import "AMDDetailedInfoViewController.h"

@interface AMDDetailedInfoViewController ()

@end

@implementation AMDDetailedInfoViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    _productNameLabel.text = [NSString stringWithFormat:@"Product Name: %@",_productName];
    _itemIDLabel.text = [NSString stringWithFormat:@"Item ID: %d", (int)_itemID];
    _upcLabel.text = [NSString stringWithFormat:@"UPC: %@",_upc];
    _aisleLabel.text = [NSString stringWithFormat:@"Aisle Label: %@",_aisle];
    
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (IBAction)doneButtonPressed:(id)sender {
    [self dismissViewControllerAnimated:YES completion:nil];
}


@end
                    

And there we have it: two ViewControllers that work together to perform Single Product Search, and handle the results in a very readable and user-friendly fashion.

Now, you will want to download this Storyboard and add it to your project. With this addition, you will be able to perform both use-cases described up to this point.

Intro + AMDCombinedViewController.h

AMDCombinedViewController will combine the previous two use-cases into a single one: viewing the map of a store, and adding the ability to find where items are located within it by searching using term or UPC searches. Found items will drop clickable pins on the map at their respective locations. The header for AMDCombinedViewController is as follows:


// AMDCombinedViewController.h

#import <UIKit/UIKit.h>

@interface AMDCombinedViewController : UIViewController

@property (strong, nonatomic) IBOutlet UITextField *storeIdArea;

@property (strong, nonatomic) IBOutlet UITextField *termArea;

@property (strong, nonatomic) IBOutlet UIButton *goButton;

@property (strong, nonatomic) IBOutlet UILabel *messageArea;

@property (strong, nonatomic) IBOutlet UIView *resultArea;


@end
                    

These properties will become more clear when seen in the implementation to follow.

AMDCombinedViewController.m

The implementation is as follows:


// AMDCombinedViewController.m

#import "AMDCombinedViewController.h"

#import "AMDGetRequester.h"

#import "MapBundle.h"
#import "MapController.h"
#import "MapBundleParser.h"
#import "FMSection.h"
#import "FMProduct.h"
#import "ProductCalloutOverlay.h"
#import "CalloutOptionOverlay.h"


@interface AMDCombinedViewController () <ProductCalloutOverlayDelegate>{
    NSArray* toUseSectionAry;
}

@end

@implementation AMDCombinedViewController{
    // Declare Necessary SDK objects
    MapBundle* mapBundle;
    MapController* mapController;
    ProductCalloutOverlay* productCalloutOverlay;
    AMDGetRequester* getRequester;
    
    // Declare Necessary storage variables
    NSString* storeID;
    NSMutableData* responseMapData;
    NSDictionary* resultDict;
    NSString* path;
    
    // To indicate wether user entered an UPC or a term
    BOOL usingUPC;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    getRequester = [[AMDGetRequester alloc] init];
    getRequester.ownerVC = self;
    
    //Setup map area
    mapController = [[MapController alloc] init];
    mapController.view.frame = _resultArea.frame;
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (IBAction)goButtonPressed:(id)sender {
    // Hide keyboards
    [_storeIdArea endEditing:YES];
    [_termArea endEditing:YES];
    
    // Display message if user input was empty
    if ([_termArea.text isEqualToString:@""] || [_storeIdArea.text isEqualToString:@""]) {
        _messageArea.textColor = [UIColor redColor];
        _messageArea.text = @"Please input both parameters";
        return;
    }
    
    _messageArea.textColor = [UIColor purpleColor];
    _messageArea.text = @"Loading Search Result...";
    
    
    /****** Following is for fetching search result ******/
    
    // To determine if user entered number(UPC) or string(term)
    NSScanner* scanner = [NSScanner scannerWithString:_termArea.text];
    NSInteger* integer = NULL;
    
    // The parameters for the getRequester
    NSArray* toPassIn;
    
    if ([scanner scanInteger:integer]) {
        toPassIn = [NSArray arrayWithObjects:[NSString stringWithFormat:@"retailer_store_id=%@", _storeIdArea.text], [NSString stringWithFormat:@"upc=%@", _termArea.text], @"start=0", @"end=7", nil];
        usingUPC = true;
    }else{
        toPassIn = [NSArray arrayWithObjects:[NSString stringWithFormat:@"retailer_store_id=%@", _storeIdArea.text], [NSString stringWithFormat:@"term=%@", _termArea.text], @"start=0", @"end=7", nil];
        usingUPC = false;
    }
    
    
    // Use the getRequester to get the request URL
    NSString* url = [getRequester SendGetRequest:@"searchproduct" withParameters:toPassIn];
    
    
    dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        // Background processing
        
        NSData* dataLoad = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:url]];
        
        if (dataLoad == nil) {
            _messageArea.textColor = [UIColor redColor];
            _messageArea.text = @"No Data Was Received";
            return;
        }
        
        dispatch_async( dispatch_get_main_queue(), ^{
            
            // Update the UI/send notifications based on the
            // Results of the background processing
            
            NSError* error;
            
            if (usingUPC) {
                resultDict = [NSJSONSerialization JSONObjectWithData:dataLoad options:NSJSONReadingMutableContainers error:&error];
            }else{
                resultDict = [NSJSONSerialization JSONObjectWithData:dataLoad options:NSJSONReadingMutableContainers error:&error];
            }
            
            
            if (error) {
                _messageArea.textColor = [UIColor redColor];
                _messageArea.text = @"Failed to Search";
            }else{
                _messageArea.textColor = [UIColor purpleColor];
                _messageArea.text = @"Success, Now Loading Map....";
            }
        });
    });
    
    
    
    
    /****** Following is for fetching map data ******/

    
    NSArray* toPassInForStoreMap = [NSArray arrayWithObject:[NSString stringWithFormat:@"retailer_store_id=%@", _storeIdArea.text]];
    storeID = _storeIdArea.text;
    
    NSString* urlForStoreMap = [getRequester SendGetRequest:@"map" withParameters:toPassInForStoreMap];
    
    dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        // Background processing
        
        NSData* urlDataForStoreMap = [NSData dataWithContentsOfURL:[NSURL URLWithString:urlForStoreMap]];
        
        if (urlDataForStoreMap == nil) {
            _messageArea.textColor = [UIColor redColor];
            _messageArea.text = @"No Data Was Received";
            return;
        }
        
        dispatch_async( dispatch_get_main_queue(), ^{
            
            // Update the UI/send notifications based on the
            // Results of the background processing
            
            if ([NSJSONSerialization JSONObjectWithData:urlDataForStoreMap options:kNilOptions error:nil] != nil) {
                _messageArea.textColor = [UIColor redColor];
                _messageArea.text = @"Map Not Found";
                for (UIView* subview in [_resultArea subviews]) {
                    [subview removeFromSuperview];
                }
                return;
            }
            
            _messageArea.textColor = [UIColor blackColor];
            _messageArea.text = @"";
            for (UIView *subview in [_resultArea subviews]) {
                [subview removeFromSuperview];
            }
            
            if (urlForStoreMap) {
                NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
                NSString* documentsDirectory = [paths objectAtIndex:0];
                NSString  *filePath = [NSString stringWithFormat:@"%@/%@", documentsDirectory, [NSString stringWithFormat:@"%@.imap", _storeIdArea]];
                [urlDataForStoreMap writeToFile:filePath atomically:YES];
                
                NSFileManager* fileMgr = [NSFileManager defaultManager];
                
                path = filePath;
                
                if ([fileMgr fileExistsAtPath:filePath]) {
                    _messageArea.textColor = [UIColor greenColor];
                    _messageArea.text = [NSString stringWithFormat:@"Found Map for ID: %@", _storeIdArea.text];
                    [self showMap];
                    [self loadProductsOverlay];
                }
                
                
            }
        });
    });
}

- (void)showMap {
    
    // To parse the fetched map data
    MapBundleParser *parser = [[MapBundleParser alloc] initWithPathToArchive:path];
    mapBundle = [parser parse];
    
    //initialize the mapController, set its view to desired view
    mapController = [[MapController alloc] init];
    mapController.mapBundle = mapBundle;
    mapController.view.frame = _resultArea.bounds;
    [_resultArea addSubview:mapController.view];
    
    [mapController setFloor:1];
    [mapController compassAction:nil];
    [mapController setCompassEnabled:NO];
    mapController.view.backgroundColor = [UIColor whiteColor];
    [mapController setLogoBottomOffset:0];
}

/*********************
 Due to returned dictionary names are different
 Also have the same contents
 This method is fairly long to handle different cases in this demo
 *********************/

- (void)loadProductsOverlay{
    
    // To add sections to the product overlay
    NSMutableArray* sectionAry = [[NSMutableArray alloc] init];
    
    if (usingUPC) {
        if (resultDict[@"products"][@"item_nm"] != nil) {
            // If the result has item_nm value, it was successfully returned one product.
            
            for (int i = 0; i < [resultDict[@"products"][@"sections"] count]; i++) {
                FMSection* tempSec = [[FMSection alloc] initWithSublocation:(int)resultDict[@"products"][@"sections"][i][@"item_sub_location_id"] location:[resultDict[@"products"][@"sections"][i][@"item_location_id"] intValue]];
                tempSec.maplocation = [resultDict[@"products"][@"sections"][i][@"map_location_id"] intValue];
                tempSec.title = resultDict[@"products"][@"item_nm"];
                tempSec.aisleTitle = resultDict[@"products"][@"sections"][i][@"aisle"];
                
                FMProduct* tempPro = [[FMProduct alloc] init];
                tempPro.name = resultDict[@"products"][@"item_nm"];
                tempPro.sections = [NSArray arrayWithObject:tempSec];
                tempPro.idn = (int)resultDict[@"products"][@"item_id"];
                
                [sectionAry addObject:tempPro];
            }
            _messageArea.text = [NSString stringWithFormat:@"Found product By UPC"];
            
        }else{
            // If the result has no such value, it mean it returned empty result
            _messageArea.textColor = [UIColor redColor];
            _messageArea.text = [NSString stringWithFormat:@"Found Nothing By UPC"];
        }
    }else{
        if (resultDict[@"products"] != nil) {
            
            // If the returned dictionary has key products, it means found a exact match
            // Another chance is that the returned dictionary is empty (Found null)
            
            for (int i = 0; i < [resultDict[@"products"] count]; i++) {
                for (int j = 0; j < [resultDict[@"products"][i][@"sections"] count]; j++) {
                    FMSection* tempSec = [[FMSection alloc] initWithSublocation:(int)resultDict[@"products"][i][@"sections"][j][@"item_sub_location_id"] location:[resultDict[@"products"][i][@"sections"][j][@"item_location_id"] intValue]];
                    tempSec.maplocation = [resultDict[@"products"][i][@"sections"][j][@"map_location_id"] intValue];
                    tempSec.title = resultDict[@"products"][i][@"synonym_nm"];
                    tempSec.aisleTitle = resultDict[@"products"][i][@"sections"][j][@"aisle"];
                    
                    FMProduct* tempPro = [[FMProduct alloc] init];
                    tempPro.name = resultDict[@"products"][i][@"synonym_nm"];
                    tempPro.sections = [NSArray arrayWithObject:tempSec];
                    tempPro.idn = (int)resultDict[@"products"][i][@"synonym_id"];
                    
                    [sectionAry addObject:tempPro];
                }
            }
            
            _messageArea.text = [NSString stringWithFormat:@"Found Exact Match or Null"];
            
        }else if (resultDict[@"product_suggestions"] != nil){
            
            // If the returned dictionary has key products,
            //it means found a list of items that can be found by the term
            
            for (int i = 0; i < [resultDict[@"product_suggestions"] count]; i++) {
                for (int j = 0; j < [resultDict[@"product_suggestions"][i][@"sections"] count]; j++) {
                    FMSection* tempSec = [[FMSection alloc] initWithSublocation:(int)resultDict[@"product_suggestions"][i][@"sections"][j][@"item_sub_location_id"] location:[resultDict[@"product_suggestions"][i][@"sections"][j][@"item_location_id"] intValue]];
                    tempSec.maplocation = [resultDict[@"product_suggestions"][i][@"sections"][j][@"map_location_id"] intValue];
                    tempSec.title = resultDict[@"product_suggestions"][i][@"synonym_nm"];
                    tempSec.aisleTitle = resultDict[@"product_suggestions"][i][@"sections"][j][@"aisle"];
                    
                    FMProduct* tempPro = [[FMProduct alloc] init];
                    tempPro.name = resultDict[@"product_suggestions"][i][@"synonym_nm"];
                    tempPro.sections = [NSArray arrayWithObject:tempSec];
                    tempPro.idn = (int)resultDict[@"product_suggestions"][i][@"synonym_id"];
                    
                    [sectionAry addObject:tempPro];
                }
            }
            
            _messageArea.text = [NSString stringWithFormat:@"Found Multiple Suggestions"];
            
        }else{
            
            // Else, the server determines that the user entered a typo
            // Then the server will return a list of suggestion words
            // Returned dictionary have typo_suggestions key
            _messageArea.textColor = [UIColor orangeColor];
            _messageArea.text = @"Typo, returned suggestions";
        }
    }
    
    toUseSectionAry = [NSArray arrayWithArray:sectionAry];
    
    productCalloutOverlay = [[ProductCalloutOverlay alloc] init];
    
    productCalloutOverlay.products = toUseSectionAry;
    
    //set up callout delegate in order to display pin title
    productCalloutOverlay.productCalloutDelegate = self;
    
    // Add the overlay to the mapController
    [mapController addOverlay:productCalloutOverlay];
}

#pragma mark - product detail from chevron button
- (void) chevronClickedForItem: (OverlayItem *)item {
    
    //leaves for future use
    NSLog(@"in chevron button");
}

#pragma mark - ProductCalloutOverlayDelegate methods
//set title for the overlay, may modify this to satisfy actually needed display
- (NSString*)titleForItem:(OverlayItem *)item{
    FMSection* section = nil;
    if ([item isKindOfClass:[ProductOverlayItem class]]) {
        ProductOverlayItem* prItem = (ProductOverlayItem*)item;
        for (int i = 0; i < [prItem.products count]; ++i) {
            FMProduct* shlItem = [self productAtIndex:i forItem:item];
            if (shlItem != nil) {
                if (shlItem.sections != nil) {
                    section = [shlItem.sections objectAtIndex:0];
                    break;
                }
            }
        }
    }
    
    if (section != nil) {
        return section.aisleTitle;
    } else {
        return @"";
    }
}

#pragma mark - to get products for an item
- (FMProduct*)productAtIndex:(NSUInteger)ind forItem:(OverlayItem *)item {
    FMProduct* shlItem = nil;
    
    if ([item isKindOfClass:[ProductOverlayItem class]]) {
        ProductOverlayItem* prItem = (ProductOverlayItem*)item;
        FMProduct* fpr = [prItem.products objectAtIndex:ind];
        
        NSArray *list = toUseSectionAry;
        for (shlItem in list) {
            if (shlItem.idn == fpr.idn) {
                break;
            }
        }
    }
    return shlItem;
}

#pragma mark - keyboard resign
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    
    UITouch *touch = [[event allTouches] anyObject];
    if ([_storeIdArea isFirstResponder] && [touch view] != _storeIdArea) {
        [_storeIdArea resignFirstResponder];
    }else{
        [_termArea resignFirstResponder];
    }
    [super touchesBegan:touches withEvent:event];
}
@end
                    

Now that the coding portion is done, you will want to download this Storyboard and add it to your project. You will then be able to access this use-case like the previous one's we've implemented.

ShoppingListVC and Associated View Controllers

ShoppingListVC is the main View Controller for utilizing Aisle411's Shopping List Search functionality. However, it is accompanied by ShoppingListMapVC and ListTableVC to keep each entity pretty simple, while still displaying the diverse capabilities of the web service. With all three of these View Controllers in tandem, you will be able to:

  • Search for a list of items by search term, UPC or a combination of the two
  • See the results of the search on the map of the store
  • See the results in a table
  • Click on individual results for more information

Without further ado, here is the header for the first ViewController, ShoppingListVC:


// ShoppingListVC.h

#import <UIKit/UIKit.h>

@interface ShopplingListVC : UIViewController
@property (strong, nonatomic) IBOutlet UITextField *searchTermTextField;
@property (strong, nonatomic) IBOutlet UITextField *storeIDtextField;
- (IBAction)goButtonAction:(id)sender;

@end
                    

ShoppingListVC.m

ShoppingListVC will handle the search for a list of items, the request that gets made, and parses the response, handing it off to one of the other View Controllers. The implementation for ShoppingListVC is as follows:


// ShoppingListVC.m

#import "ShopplingListVC.h"
#import "ShopplingListMapVC.h"

@interface ShopplingListVC () <UITextFieldDelegate>

@end

@implementation ShopplingListVC

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    _storeIDtextField.delegate = self;
    _searchTermTextField.delegate = self;
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (IBAction)goButtonAction:(id)sender {
    
    if ([_storeIDtextField.text isEqualToString:@""]) {
        UIAlertController *alert = [UIAlertController
                                    alertControllerWithTitle:@"Error"
                                    message:@"Please type in store ID"
                                    preferredStyle:UIAlertControllerStyleAlert];
        UIAlertAction *confirm = [UIAlertAction
                                  actionWithTitle:@"Confirm"
                                  style:UIAlertActionStyleDefault
                                  handler: nil];
        [alert addAction: confirm];
        [self presentViewController:alert animated:YES completion:nil];
    }
    
    if (![_storeIDtextField.text isEqualToString:@""]){
        [self performSegueWithIdentifier:@"pushToMap" sender:sender];
    }
}

- (BOOL)textFieldShouldReturn:(UITextField *)textField{
    if (textField.text.length==0) {
        [textField resignFirstResponder];
        return NO;
    }
    return YES;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    
    UITouch *touch = [[event allTouches] anyObject];
    if ([_storeIDtextField isFirstResponder] && [touch view] != _storeIDtextField) {
        [_storeIDtextField resignFirstResponder];
    }
    else if ([_searchTermTextField isFirstResponder] && [touch view] != _searchTermTextField) {
        [_searchTermTextField resignFirstResponder];
    }
    [super touchesBegan:touches withEvent:event];
}

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
    if ([segue.identifier isEqualToString:@"pushToMap"]) {
        ShopplingListMapVC *mapView = [segue destinationViewController];
        mapView.storeID = _storeIDtextField.text;
        mapView.searchTerms = _searchTermTextField.text;
    }
}

@end
                    

You'll want to move forward with the next ViewControllers before you try out our new invention. The next will be ShoppingListMapVC.

Intro + ShoppingListMapVC.h

ShoppingListMapVC will be the View Controller that displays list search results on the map as pins for each item found in the store. This is very similar to Single Product Search, and expanded views for these results can be created in a similar way as before, but this is not included in this demo. The header for this class is as follows:


// ShoppingListMapVC.h

#import <UIKit/UIKit.h>
#import "MapBundle.h"
#import "MapController.h"
#import "MapBundleParser.h"
#import "AMDGetRequester.h"
#import "FMSection.h"
#import "FMProduct.h"
#import "ProductCalloutOverlay.h"
#import "CalloutOptionOverlay.h"

@interface ShopplingListMapVC : UIViewController
@property (strong, nonatomic) IBOutlet UIView *mapView;
@property (strong, nonatomic) NSString *searchTerms;
@property (strong, nonatomic) NSString *storeID;

- (IBAction)listButton:(id)sender;


@end
                    

This class imports lots of classes from the MapSDK and AMDGetRequester, which we implemented earlier. The results from the search entered in the ShoppingListVC will appear on this View Controller. Let's get to the implementation.

ShoppingListMapVC.m

With the implementation, you will be able to see the pins appear on the map for the items found from your shopping list search. The implementation is as follows:


// ShoppingListMapVC.m

#import "ShopplingListMapVC.h"
#import "ListTableVC.h"

@interface ShopplingListMapVC ()<ProductCalloutOverlayDelegate>{
    NSMutableArray* jsonArray;//To store returned JSON
    MapBundle* mapBundle;
    MapController* mapController;
    AMDGetRequester* getRequester;
    NSString* path;
    UIActivityIndicatorView *indicator;
    UIView *indicatorBackground;
    NSDictionary* resultDict;
    NSDictionary *searchedResults;
    ProductCalloutOverlay* productCalloutOverlay;
    NSMutableArray *productSectionArray;    
}
@end

@implementation ShopplingListMapVC

- (void)viewDidLoad {
    [super viewDidLoad];
    getRequester = [[AMDGetRequester alloc] init];
    getRequester.ownerVC = self;
    
    //Setup map area
    mapController = [[MapController alloc] init];
    mapController.view.frame = _mapView.frame;
    
    [self getMapFromServer];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}

- (void)showMap{
    // To parse the fetched map data
    MapBundleParser *parser = [[MapBundleParser alloc] initWithPathToArchive:path];
    mapBundle = [parser parse];
    
    //initialize the mapController, set its view to desired view
    mapController = [[MapController alloc] init];
    mapController.mapBundle = mapBundle;
    mapController.view.frame = _mapView.bounds;
    [_mapView addSubview:mapController.view];
    
    [mapController setFloor:1];
    [mapController compassAction:nil];
    [mapController setCompassEnabled:NO];
    mapController.view.backgroundColor = [UIColor whiteColor];
    [mapController setLogoBottomOffset:0];
    [self shoppinglistResultFromServer];//////
}

- (void)getMapFromServer{
    [self addIndicatorView];
    NSArray* toPassIn = [NSArray arrayWithObject:[NSString stringWithFormat:@"retailer_store_id=%@", _storeID]];
    
    // Use the getRequester to get the request URL
    NSString* url = [getRequester SendGetRequest:@"map" withParameters:toPassIn];
    
    dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        //get the last-modified time stamp from server
        __block NSString *server_timestamp;
        NSURL *realurl = [NSURL URLWithString:url];
        //            NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
        //            NSHTTPURLResponse *response;
        //            [NSURLConnection sendSynchronousRequest: request returningResponse: &response error: nil];
        NSURLSession *session = [NSURLSession sharedSession];
        [[session dataTaskWithURL: realurl completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
            NSHTTPURLResponse *httpresp = (NSHTTPURLResponse *) response;
            if ([httpresp respondsToSelector:@selector(allHeaderFields)]){
                NSDictionary *dictionary = [httpresp allHeaderFields];
                server_timestamp = [dictionary objectForKey:@"Last-Modified"];
                NSLog(@"server timestamp is %@",server_timestamp);
            }
        }] resume];
        
        //check the map timestamp locally
        NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
        NSString *time_stamp = [defaults objectForKey:_storeID];
        NSLog(@"user_defalut timestamp is %@", time_stamp);
        
        //if timestamp doesn't exist or is not equal to the server
        if(![server_timestamp isEqualToString:time_stamp]){
            
            //update timestamp locally
            [defaults setObject:server_timestamp forKey:_storeID];
            [defaults synchronize];
            NSLog(@"now user_defalut timestamp is %@",[defaults objectForKey:_storeID]);
            
            // Background processing
            NSData* urlData = [NSData dataWithContentsOfURL:[NSURL URLWithString:url]];
            if (urlData == nil) {
                UIAlertController *alert = [UIAlertController
                                             alertControllerWithTitle:@"Error"
                                             message:@"No Data Was Received"
                                             preferredStyle:UIAlertControllerStyleAlert];
                UIAlertAction *confirm = [UIAlertAction
                                          actionWithTitle:@"Confirm"
                                          style:UIAlertActionStyleDefault
                                          handler: nil];
                [alert addAction: confirm];
                [self presentViewController:alert animated:YES completion:nil];
                return;
            }
            
            // Update the UI/send notifications based on the
            // Results of the background processing
            dispatch_async( dispatch_get_main_queue(), ^{
                [self removeIndicatorView];
                if ([NSJSONSerialization JSONObjectWithData:urlData options:kNilOptions error:nil] != nil) {
                    NSLog(@"JSON DETECTED");
                    UIAlertController *alert = [UIAlertController
                                                alertControllerWithTitle:@"Error"
                                                message:@"Map Not Found"
                                                preferredStyle:UIAlertControllerStyleAlert];
                    UIAlertAction *confirm = [UIAlertAction
                                              actionWithTitle:@"Confirm"
                                              style:UIAlertActionStyleDefault
                                              handler: nil];
                    [alert addAction: confirm];
                    [self presentViewController:alert animated:YES completion:nil];
                    for (UIView *subview in [_mapView subviews]) {
                        [subview removeFromSuperview];
                    }
                    return;
                }
                
                for (UIView *subview in [_mapView subviews]) {
                    [subview removeFromSuperview];
                }
                
                if (urlData) {
                    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
                    NSString  *documentsDirectory = [paths objectAtIndex:0];
                    
                    NSString  *filePath = [NSString stringWithFormat:@"%@/%@", documentsDirectory, [NSString stringWithFormat:@"%@.imap", _storeID]];
                    [urlData writeToFile:filePath atomically:YES];
                    
                    NSFileManager* fileMgr = [NSFileManager defaultManager];
                    path = filePath;
                    NSLog(@"new path is %@",path);
                    if ([fileMgr fileExistsAtPath:filePath]) {
                        NSLog(@"store map locally!");
                        [self showMap];
                    }
                    
                }else{
                    UIAlertController *alert = [UIAlertController
                                                alertControllerWithTitle:@"Error"
                                                message:@"No Data Was Received"
                                                preferredStyle:UIAlertControllerStyleAlert];
                    UIAlertAction *confirm = [UIAlertAction
                                              actionWithTitle:@"Confirm"
                                              style:UIAlertActionStyleDefault
                                              handler: nil];
                    [alert addAction: confirm];
                    [self presentViewController:alert animated:YES completion:nil];
                }
            });
        }
        else{
            //use cached map
            dispatch_async( dispatch_get_main_queue(), ^{
                [self removeIndicatorView];
                
                for (UIView *subview in [_mapView subviews]) {
                    [subview removeFromSuperview];
                }
                
                NSArray *exist_paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
                NSString  *exist_documentsDirectory = [exist_paths objectAtIndex:0];
                
                NSString* exist_filePath = [exist_documentsDirectory stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.imap", _storeID] ];
                
                NSFileManager* fileMgr = [NSFileManager defaultManager];
                
                path = exist_filePath;
                NSLog(@"the exist path is %@",path);
                if ([fileMgr fileExistsAtPath:exist_filePath]) {
                    NSLog(@"found map!");
                    [self showMap];
                }else{
                    UIAlertController *alert = [UIAlertController
                                                alertControllerWithTitle:@"Error"
                                                message:@"The map is missing!"
                                                preferredStyle:UIAlertControllerStyleAlert];
                    UIAlertAction *confirm = [UIAlertAction
                                              actionWithTitle:@"Confirm"
                                              style:UIAlertActionStyleDefault
                                              handler: nil];
                    [alert addAction: confirm];
                    [self presentViewController:alert animated:YES completion:nil];
                    [defaults setObject:@"" forKey:_storeID];
                    [defaults synchronize];
                }
            });
        }
    });
}

#pragma mark - get shopping list search results
- (void)shoppinglistResultFromServer{
    NSString* userInfoPath = [[NSBundle mainBundle] pathForResource:@"UserInfo" ofType:@"plist"];
    NSDictionary* userInfoDict = [[NSDictionary alloc] initWithContentsOfFile:userInfoPath];
    
    //create user search term dictionary
    NSArray* userInputArray = [_searchTerms componentsSeparatedByString:@","];
    NSMutableArray *items = [[NSMutableArray alloc] init];
    NSMutableDictionary *item;
    for(NSString *term in userInputArray)
    {
        item = [[NSMutableDictionary alloc] init];
        [item setValue:term forKey:@"name"];
        //[item setValue:@"1" forKey:@"quantity"];
        [items addObject:item];
    }
    
    NSMutableDictionary *userSearchTermDict = [[NSMutableDictionary alloc] init];
    [userSearchTermDict setValue:@"demoList" forKey:@"name"];
    [userSearchTermDict setValue:items forKey:@"items"];
    
    //set parameters for the post method
    NSMutableDictionary *arguments = [[NSMutableDictionary alloc] init];
    [arguments setValue:userInfoDict[@"partnerIdentifier"] forKey:@"partner_id"];
    [arguments setValue:_storeID forKey:@"retailer_store_id"];
    [arguments setValue:userSearchTermDict forKey:@"shopping_list_data"];
    [arguments setValue:[getRequester deviceToken] forKey:@"device_token"];
    
    [getRequester sendWithPostMethodAndCompletionHandler:arguments block:^(NSDictionary *resultDictionary){
        searchedResults=resultDictionary;
        [self createProductPins];
    }];
}

- (void)createProductPins{
    NSLog(@"%@",searchedResults);
    NSArray *resultArray= [searchedResults objectForKey:@"items"];
    productSectionArray = [[NSMutableArray alloc] init];
    NSMutableArray *productsArray = [[NSMutableArray alloc] init];
    
    for (NSDictionary *temp in resultArray) {
        FMProduct *prod = [[FMProduct alloc] init];
        if(temp[@"synonym_nm"]){
            prod.name = [temp objectForKey:@"synonym_nm"];
            prod.idn = [[temp objectForKey:@"synonym_id"] intValue];
        } else {
            prod.name = temp[@"name"];
        }
        
        NSArray* tempArray=temp[@"sections"];
        NSDictionary* actualItemDict=tempArray[0];
        
        FMSection *sec = [[FMSection alloc] init];
        sec.sublocation = [actualItemDict[@"item_sub_location_id"] intValue];
        sec.location = [actualItemDict[@"item_location_id"] intValue];
        sec.maplocation = [actualItemDict[@"map_location_id"] intValue];
        sec.aisleTitle =actualItemDict[@"aisle"];
        sec.title = actualItemDict[@"section"];
        
        [productSectionArray addObject:sec];
        
        prod.sections = [[NSMutableArray alloc] initWithArray:productSectionArray];
        [productsArray addObject:prod];

        [productSectionArray removeAllObjects];
    }
    
    //add overlay to map
    productCalloutOverlay = [[ProductCalloutOverlay alloc] init];
    productCalloutOverlay.products = productsArray;
    
    //set up callout delegate in order to display pin title
    productCalloutOverlay.productCalloutDelegate = self;
    
    // Add the overlay to the mapController
    [mapController addOverlay:productCalloutOverlay];

}

#pragma mark - product detail from chevron button
- (void) chevronClickedForItem: (OverlayItem *)item {
    //leaves for future use
    NSLog(@"in chevron button");
}

#pragma mark - ProductCalloutOverlayDelegate methods
//set title for the overlay, may modify this to satisfy actually needed display
- (NSString*)titleForItem:(OverlayItem *)item{
    FMSection* section = nil;
    if ([item isKindOfClass:[ProductOverlayItem class]]) {
        ProductOverlayItem* prItem = (ProductOverlayItem*)item;
        for (int i = 0; i < [prItem.products count]; ++i) {
            FMProduct* shlItem = [self productAtIndex:i forItem:item];
            if (shlItem != nil) {
                if (shlItem.sections != nil) {
                    section = [shlItem.sections objectAtIndex:0];
                    break;
                }
            }
        }
    }
    
    if (section != nil) {
        NSLog(@"%@",section.aisleTitle);
        return section.aisleTitle;
    } else {
        return @"Store";
    }
}

#pragma mark - to get products for an item
- (FMProduct*)productAtIndex:(NSUInteger)ind forItem:(OverlayItem *)item {
    FMProduct* shlItem = nil;
    
    if ([item isKindOfClass:[ProductOverlayItem class]]) {
        ProductOverlayItem* prItem = (ProductOverlayItem*)item;
        FMProduct* fpr = [prItem.products objectAtIndex:ind];
        
        NSArray *list = productSectionArray;
        for (shlItem in list) {
            if (shlItem.idn == fpr.idn) {
                break;
            }
        }
    }
    return shlItem;
}


#pragma mark - segue related
- (IBAction)listButton:(id)sender {
    [self performSegueWithIdentifier:@"itemList" sender:self];
}

-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
    if ([segue.identifier isEqualToString:@"itemList"]) {
        ListTableVC *tableView = [segue destinationViewController];
        tableView.listDict = searchedResults;
    }
}

#pragma mark - indicator view related

-(void)addIndicatorView{
    indicatorBackground = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 50, 50)];
    indicatorBackground.center = self.view.center;
    indicatorBackground.layer.cornerRadius = 10;
    indicatorBackground.backgroundColor = [UIColor grayColor];
    
    indicator = [[UIActivityIndicatorView alloc] initWithFrame:CGRectMake(0, 0, 25, 25)];
    indicator.color = [UIColor blueColor];
    indicator.center = self.view.center;
    
    [indicator startAnimating];
    [self.view addSubview:indicatorBackground];
    [self.view addSubview:indicator];
}

-(void)removeIndicatorView{
    [indicatorBackground removeFromSuperview];
    [indicator removeFromSuperview];
}


@end
                    

Lastly, we move on to ListTableVC, the finally View Controller to complete this demo of Aisle411's web services.

Intro + ListTableVC.h

The ListTableVC is quite simple. Its role is just to display the shopping list search results in a table format rather than as pins across the map. This is extremely useful for longer shopping lists, and also allows for the user to cross off items that have already been picked up. The header for this class is as follows:


// ListTableVC.h

#import <UIKit/UIKit.h>

@interface ListTableVC : UITableViewController
@property (nonatomic,weak) NSDictionary *listDict;

@end
                    

This header is pretty simple, so let's move right on to the implementation!

ListTableVC.m

The implementation for ListTableVC is as follows:


// ListTableVC.m

#import "ListTableVC.h"

@interface ListTableVC ()

@end

@implementation ListTableVC

- (void)viewDidLoad {
    [super viewDidLoad];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return [[_listDict objectForKey:@"items"] count];
}


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"itemCell" forIndexPath:indexPath];
    
    NSArray *itemArray = [_listDict objectForKey:@"items"];
    
    //get current item dict
    NSDictionary *itemDict = [itemArray objectAtIndex:indexPath.row];
    
    //get item section array
    NSArray *sectionArray =itemDict[@"sections"];
    
    //convert array into dictionary
    NSDictionary *sectionDict = (NSDictionary *)sectionArray[0];

    //set up label
    cell.textLabel.text = [itemDict objectForKey:@"name"];
    cell.detailTextLabel.text = [NSString stringWithFormat:@"Section: %@ -- Aisle: %@",[sectionDict objectForKey:@"section"],[sectionDict objectForKey:@"aisle"]];
    
    return cell;
}

/*
// Override to support conditional editing of the table view.
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
    // Return NO if you do not want the specified item to be editable.
    return YES;
}
*/

/*
// Override to support editing the table view.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
    if (editingStyle == UITableViewCellEditingStyleDelete) {
        // Delete the row from the data source
        [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
    } else if (editingStyle == UITableViewCellEditingStyleInsert) {
        // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
    }   
}
*/

/*
// Override to support rearranging the table view.
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
}
*/

/*
// Override to support conditional rearranging of the table view.
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath {
    // Return NO if you do not want the item to be re-orderable.
    return YES;
}
*/

/*
#pragma mark - Navigation

// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    // Get the new view controller using [segue destinationViewController].
    // Pass the selected object to the new view controller.
}
*/

@end
                    

This implementation mostly implements the ability to display results in a table. Possible add-ons are hinted at in descriptive comments throughout the file. One this implementation is complete, we can take steps to try out Shopping List Search in its full glory. You will want to download this Storyboard and add it to your project. With this finished, you will be able to enjoy the demo in its full glory, as we are done with our walkthrough.

If you would like to download the demo in it's entirety, click here.

Should you have any questions about the demo, our web services, or anything else Aisle411, feel free to reach out to us at: tech (at) aisle411 (dot) com, with any inquiries.