iOS Register Hardware


#1

I have an iOS app I am building that allows DEMO and registered live users using DF and Mongo. I want to have total insight and control over the app on devices to include licensing and logging every new launch, last launch, location, app information, device info and also be able to manage and even inactivate devices from launching my app. I am porting…umm…actually rebuilding apps I have now that are using PARSE.

I wanted to share my efforts hoping someone who has a similar need and a cure for pattern baldness would consider rewarding me. I am using the design pattern in the DF iOS Sample App, so download / install that and implement this code to see if it works for you.

Cheers!


First thing, we want to set up DF to allow all devices to communicate with the APIs without a user session in order to start immediately tracking new installs. (I’m passing along my naming convention but they can be whatever you want)

  • Assuming you have a DF app already set up with an API Key.
  1. Create a ‘Device’ table in your database. You don’t need to add any fields or schema if Mongo - not sure about SQL dB’s.

  2. Create a new Role called Devices. Go to the Access Tab and create a new Service. Select your DF from the drop down, then find and select ‘_tables/Devices’. Click on ‘Access’ and select GET POST PATCH.

  3. Create another Service for ‘_tables/Devices*’ and select GET POST PATCH Access then save the Role

  4. Go to Apps and create a new app called Devices or whatever you like. Select the ‘No Storage…’ option and then select and assign the new Role you just created. Save the app, then reopen the app and you should have an API Key there.

  5. Go to your Services Tab and select the User Management service. Then click the Config tab and select 'Open Registration. Next assign the Devices Role to the ‘Open Reg Role’ and the ‘Update Service’.

****NOTE: I’ve been reading how to allow open registration, etc and everyone keeps saying to enable CORS with *. I did adn everything following worked fine - BUT - I also deleted the CORS * record and tried it and everything still works fine. I don’t know anything about CORS outside of Golden, CO so this is either a bug or you don’t need to enable it for open registration. Smart people please feel free to comment.

DSP setup complete.


Next is the code - I pulled into my project the RESTEngine* and NKI* files from the DF iOS sample appt to use as a basis for my app/DF communication. I highly recommend anyone who isn’t an iOS guru to do this as there are some methods and such in there that look complicated but seem to tie everything together…

I am going to throw in some iOS specific suggestions to show you how I accomplished my goal of Device Management.

  1. Drag and drop the RESTEngine and NKI* from the DF iOS Sample App. We are going to be working with the RESTEngine files. For me, as I continue to build my app and learn the intricacies of the API’s, I do not delete or comment anything out, just return a lot of space between their (well-documented) code and my new code so I can refer to it as needed.

  2. Make your definitions. In the top of the RESTEngine.h you’ll see some definitions (placeholders) for your API Key, etc. I modified mine a bit to break #define kDbServiceName @"db/_table" into 2 parts: #define kService @"/app" and #define kDbServiceName @"/_table". I did this for future consideration in the event the app wanted to communicate with another service in DF. Following is how I set up my definitions:

//PATHS
#define kBaseUrl @"http://<your hostname>/api/v2"
#define kBaseIp @"<your host IP or hostname>" (I use this for reach-ability / Internet check)
#define kService @"/<your core service name>"
#define kDbServiceName @"/_table"

//Header API Key Parameters
#define kApiKey @"<API key for your core app>"
#define kDeviceApiKey @"<api key for the Device app>"
#define kHeaderApiKeyParam @"X-DreamFactory-Api-Key"
#define kContentType @"application/json"

//user services
#define kSessionTokenKey @"SessionToken"
#define kUserEmail @"UserEmail"
#define kPassword @"UserPassword"

  1. In your RESTEngine.h file, add the following method properties:

- (void)updateDeviceWithDeviceId:(NSString *)deviceId deviceDetails:(NSDictionary *)deviceDetails success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock;

- (void)addDeviceWithDetails:(NSDictionary *)deviceDetails success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock;

- (void)getDeviceInfoWithDeviceId:(NSString *)deviceId success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock;

  1. In your RESTEngine,m file, add the following methods:
- (void)addDeviceWithDetails:(NSDictionary *)deviceDetails success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock {
    
    // set an id for the NSDictionary to create the device
    id body = deviceDetails;
    
    // create device header that uses the API for Devices
    NSMutableDictionary *deviceHeaderParams = [[NSMutableDictionary alloc] init];
    [deviceHeaderParams setObject:kDeviceApiKey forKey:kHeaderApiKeyParam];
    
    [self callApiWithPath:[Routing serviceWithTableName:@"/Device"] method:@"POST" queryParams:nil body:body headerParams:deviceHeaderParams success:successBlock failure:failureBlock];
}
- (void)updateDeviceWithDeviceId:(NSString *)deviceId deviceDetails:(NSDictionary *)deviceDetails success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock {
    
    // set an id for the NSDictionary to update the device
    id body = deviceDetails;
    
    // create filter to find and update info only for this device
    NSDictionary *queryParams = @{@"filter": [NSString stringWithFormat:@"deviceId=%@", deviceId]};
    
    // create device header that uses the API for Devices
    NSMutableDictionary *deviceHeaderParams = [[NSMutableDictionary alloc] init];
    [deviceHeaderParams setObject:kDeviceApiKey forKey:kHeaderApiKeyParam];
    
    [self callApiWithPath:[Routing serviceWithTableName:@"/Device"] method:@"PATCH" queryParams:queryParams body:body headerParams:deviceHeaderParams success:successBlock failure:failureBlock];
    
}
  • (void)getDeviceInfoWithDeviceId:(NSString *)deviceId success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock
    {
    // create filter to get info only for this device
    NSDictionary *queryParams = @{@“filter”: [NSString stringWithFormat:@“deviceId=%@”, deviceId]};

    NSMutableDictionary *deviceHeaderParams = [[NSMutableDictionary alloc] init];
    [deviceHeaderParams setObject:kDeviceApiKey forKey:kHeaderApiKeyParam];

[self callApiWithPath:[Routing serviceWithTableName:@"/Device"] method:@"GET" queryParams:queryParams body:nil headerParams:deviceHeaderParams success:successBlock failure:failureBlock];

}

  1. Go to your Main Interface - the one that launches after the Launch Screen / appDelegate loads. For me it is Login.h and Login.m. Now bare with me - I’m gong to lay it all out here on how I get check internet, get location, get device details and system information etc on the device and either ADD it to the database (new installs) or update existing installs…take it or leave it or do it your own way…

  2. Download and copy into your project he following helpers:

  • Apple Reachability .h and .m [here](h ttps://developer.apple.com/library/ios/samplecode/Reachability/Introduction/Intro.html)
  • UIKeyChainStore .h and .m here
  • UIDeviceHardware .h and .m here

Read the requirements and import required Frameworks. There are a few other private helpers like a ‘Please wait’ HUD and Alerts that I’ll comment on when needed or suggested that you can figure out the best source control on GIT or write yourself.

  1. Import CoreLocation and SystemConfiguration Frameworks to your project

  2. In Login.h import the following headers:

#import "RESTEngine.h"
#import "NIKApiInvoker.h"
#import "UIDeviceHardware.h"
#import "UICKeychainStore.h"
#import "Reachability.h"

  1. In Login.h add the following to the @interface

@interface LoginVC : UIViewController <UIApplicationDelegate, CLLocationManagerDelegate> {

CLLocationManager *locationManager;

NSString *deviceId;
NSString *appName;

BOOL isFirstRun;
BOOL didFindLocation;
BOOL rememberMe;

NSString *geoLong;
NSString *geoLat;
NSMutableDictionary *deviceDetails;
}

  1. In Login.h add the following to properties below the interface

@property (nonatomic) Reachability *hostReachability;
@property (nonatomic) Reachability *internetReachability;
@property (nonatomic) Reachability *wifiReachability;

  1. Now go to your Login.m file. In the ViewDidLoad, I start checking for Internet connection:
- (void)viewDidLoad
{
    [super viewDidLoad];
    self.hostReachability = [Reachability reachabilityWithHostName:kBaseIp];
    [self.hostReachability startNotifier];
    
    self.internetReachability = [Reachability reachabilityForInternetConnection];
    [self.internetReachability startNotifier];
    
    self.wifiReachability = [Reachability reachabilityForLocalWiFi];
    [self.wifiReachability startNotifier];
    
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reachabilityChanged:) name:kReachabilityChangedNotification object: nil];
    [self logReachability:self.hostReachability];
    [self logReachability:self.internetReachability];
    [self logReachability:self.wifiReachability];
}
  1. Add the NSNotification and associated method for reachability somewhere after ViewDidLoad
- (void)reachabilityChanged:(NSNotification *)notification {
    Reachability *reachability = [notification object];
    [self logReachability: reachability];
}
- (void)logReachability:(Reachability *)reachability {
    NSString *whichReachabilityString = nil;
    
    if (reachability == self.hostReachability) {
        whichReachabilityString = @"CoreApp Reachable";
    } else if (reachability == self.internetReachability) {
        whichReachabilityString = @"The Internet";
    } else if (reachability == self.wifiReachability) {
        whichReachabilityString = @"Local Wi-Fi";
    } else {
        whichReachabilityString = @"Cell Data";
    }
    NSString *howReachableString = nil;
    
    switch (reachability.currentReachabilityStatus) {
        case NotReachable: {
            howReachableString = @"not reachable";
            // Probably good place for alert
            break;
        }
        case ReachableViaWWAN: {
            howReachableString = @"reachable by cellular data";
            break;
        }
        case ReachableViaWiFi: {
            howReachableString = @"reachable by Wi-Fi";
            break;
        }
    }
`    NSLog(@"%@ %@", whichReachabilityString, howReachableString);`
}
  1. In the ViewDidAppear, start the location services and place the location services method somewhere below that. You’ll notice that these methods kicks off other method to check device license.

-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:YES];

    [self startLocationServices];

}

-(void)startLocationServices {

locationManager = [[CLLocationManager alloc] init];
locationManager.delegate = self;

if ([locationManager respondsToSelector:@selector(requestWhenInUseAuthorization)])
{
    [locationManager requestWhenInUseAuthorization];
}
didFindLocation = NO;
[locationManager setDesiredAccuracy:kCLLocationAccuracyBest];
[locationManager startUpdatingLocation];

}

-(void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error{

UIAlertView *errorAlert = [[UIAlertView alloc]initWithTitle:NSLocalizedString(@"Configuration Error", nil) message:NSLocalizedString(@"There was an error retrieving your current location.", nil) delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil];
[errorAlert show];

LOGI(@"LOCATION ERROR: %@",error.description);

//You can alert here and or/continue on to check device license…

[self checkAppLicense];

}

-(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations {

if (!didFindLocation) {
    didFindLocation = YES;
    [locationManager stopUpdatingLocation];
    CLLocation *currentLocation = [locations lastObject];
    geoLat = [NSString stringWithFormat:@"%.8f",currentLocation.coordinate.latitude];
    geoLong = [NSString stringWithFormat:@"%.8f",currentLocation.coordinate.longitude];
    LOGI(@"DID FIND LOCATION %@", locations);

// go to next method and check for app license
[self checkAppLicense];

} 

}

  1. the -(void)checkAppLicense {} method looks to the Keychain for a deviceId. If present then the device has launched the app already (then we’ll update the device record) or not (then we’ll create a new device record). One thing to note: You can use Keychain or NSUserDefaults to store the ‘deviceId’ value. I use the keychain because I want the deviceId to persist even if user uninstalls / reinstalls app. This was I won’t have multiple deviceIds for the same app in the database.

Below the Locations Services, add the following code to check the device license and launch another method:

-(void)checkiPayLicense {

UICKeyChainStore *device = [[UICKeyChainStore alloc]init];
deviceId = [device stringForKey:@"deviceId"];

//Use for testing.  Uncomment to create new device, comment out to update device.
//  deviceId[@"deviceId"] = nil;
//  deviceId = @"";

if ([deviceId isEqual: @""] || deviceId == nil) {
     //this will tell us to use the ADD device method
    isFirstRun = YES; 
     //I use Apple's UUID method, however you can use whatever random generator that suits your needs.
    deviceId = [[NSUUID UUID] UUIDString];
     //update the keychain with new deviceId
    [Device setString:deviceId forKey:@"deviceId"];
}
 // now on to next method to start collecting info on the device
[self setDeviceDetails];
NSLog(@"iPayDeviceId %@", iPayDeviceId);

}

  1. Now we are going to collect all the device details that are available.

-(void)setDeviceDetails {

// Here we use the UIDeviceHardware helper simply to get what type of device we have here. For example, iPhone 12Gxr
UIDeviceHardware *h = [[UIDeviceHardware alloc] init];
NSString *devicePlatform = [h platformString];

// I’m a newbie at Mongo and couldn’t figure out how to automatically set a Mongodate field and timeDateStamp, so I just decided to create a text representation of it

NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init];
[dateFormat setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSSZ"];
NSString *timeDate = [dateFormat stringFromDate:[NSDate date]];
NSLog(@"LAUNCH DATETIME %@", timeDate);

//here we create an NSDictionary of all the device properties we want to record called deviceDetails. Remember the methods we set in the RESTEngine that called for an (NSDictionary *)deviceDetails ?

deviceDetails = [[NSMutableDictionary alloc]initWithObjectsAndKeys:
          
          [[UIDevice currentDevice]systemName], @"deviceSystemName",
          [[UIDevice currentDevice] name], @"deviceName",
          [[UIDevice currentDevice] localizedModel], @"deviceLocalizedModel",
          [[NSLocale preferredLanguages] objectAtIndex:0], @"deviceLanguage",
          [[UIDevice currentDevice] systemVersion], @"deviceSystemVersion",
          [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"], @"appVersion",
          [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleName"], @"appName",
          [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"], @"appRelease",
          [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleIdentifier"], @"appBundle",
          devicePlatform, @"devicePlatform",
          geoLat, @"geoLat",
          geoLong, @"geoLong",
          deviceId, @"deviceId", nil];

if (isFirstRun) {
    //add a few more details for the new record we are going to create.  Here we are going to set isActive = TRUE.  Now that we have that property, an administrator can set it to FALSE in the database and next time the app is launched you can check this BOOL and take action if it is FALSE.
    [deviceDetails setObject:[NSNumber numberWithBool:YES] forKey:@"isActive"];
    [deviceDetails setObject:timeDate forKey:@"dateCreated"];
    
    // remember we set the flag firstRun=TRUE when we didn't find a deviceId on keychain.  So now we can isolate a new method specifically for ADDING objects.
    [self createNewDevice];
} else {
    // just want to update the NSDictionary deviceDetails with a dateUpdated flag so I can check on activity and for example find devices that are current or that haven't launched for a while.
    [deviceDetails setObject:timeDate forKey:@"dateUpdated"];

// isFirstRun != TRUE so this must be an existing device we need to update…
[self updateDevice];
}
}

  1. Here is the method to CREATE A NEW DEVICE in the database

-(void)createNewDevice {

//Probably a good spot for a “Please Wait…” HUD

[[RESTEngine sharedEngine] addDeviceWithDetails:**deviceDetails** success:^(NSDictionary *response) {

// okay success response. The response only returns the objects table unique Id and I haven’t figured out how to get the full record back (any help here?). So on success we need to get the full record
[self getDeviceInfoWithDeviceId];

} failure:^(NSError *error) {

// good spot for an Alert. What you do with it and how important getting device details is up to your requirements. For me I just pass and let the user continue…
LOGI(@“Error Creating Device: %@”,error);

    });
}];

}

  1. Here is the method to UPDATE AN EXISTING DEVICE in the database

-(void)updateDevice {

[[RESTEngine sharedEngine] updateDeviceWithDeviceId:**deviceId** deviceDetails:**deviceDetails** success:^(NSDictionary *response)
{
//All is good, still haven't figured out how to get a full field response back from the DF Mongo setup yet...so I need to get the full record in another call.
    [self getDeviceInfoWithDeviceId];
    
} failure:^(NSError *error) {

//if you want to continue with the existing record that hasn’t been updated this go-around, add [self getDeviceInfoWithDeviceId]; here

    LOGI(@"Error Updating Device: %@",error);
    });
    
}];

}

  1. FINALLY (this was a lot longer than I thought…) we fetch the current device details in full. I want to have this data available to me on-demand for me to grab whenever needed, so I created a singleton for device and store the fetched results there. No way getting into that now, plenty of tuts out there - You can do whatever makes you happy…

-(void)getDeviceInfoWithDeviceId {

[[RESTEngine sharedEngine] getDeviceInfoWithDeviceId:iPayDeviceId success:^(NSDictionary *response) {

//good place to Close your HUD

    for (NSDictionary *newObject in response[@"resource"]) {

//here is where I store it into an object to fetch whenever necessary.

        [[deviceControl controller]setStorage:@"deviceObject" object:newObject];
        NSLOG(@"Device Object %@",[[deviceControl controller]getStorage:@"deviceObject"]);
    }
} failure:^(NSError *error) {
    
    LOGI(@"ERROR GETTING DEVICE INFO: %@",error);

//Close your HUD here also

// done, your login screen should be available for input now.

}];

}