oAuth2 with iOS (Part 4)

Within this post we continue working on the iOS app. For previous posts in this series, see here :).
In this part we are going to add all network functionality we need to have our app working.

Like before, all code can be found in GIT. The code like at the end of the project can be found here.

Due to the time since the previous part (Sorry :(!), iOS was upgraded a few times. With this part, everything is tested under iOS 8 BETA 4 with xCode BETA 4 as well.

We are going to implement certain ways for refreshing data into our app. First off, we add a manual refresh, including code for doing the refresh. After that, we creating new Demos, including refreshing, and we create a simple background fetch.

For getting the data from the database, we always need a account from the oAuth provider. Because this account is checked in the DemoViewController, and our background fetch which we are going to implement runs from the appDelegate, we need the account there as well.
First, lets create the property in the .m file:

@property (nonatomic, strong) NXOAuth2Account *account;

Now, in our DemoTableViewController, we can set this property in the setter for account:

- (void)setAccount:(NXOAuth2Account *)account
{
    _account = account;
    
    AppDelegate* app = ((AppDelegate *)[UIApplication sharedApplication].delegate);
    app.account = account;
    
    [self updateUI];
}

Using this account will follow up later in this part :).

Because we want to fetch stuff at certain places within our app, I modified the Category we created earlier for the Demo with the next method. In the interface:

+ (void)getDataFromClient:(NXOAuth2Account *)account
   inManagedObjectContent:(NSManagedObjectContext *)context
                  refresh:(UIRefreshControl *)refresh;

Because we use some new stuff, we also need to add two imports:

#import 
#import 

And the method:

+ (void)getDataFromClient:(NXOAuth2Account *)account
   inManagedObjectContent:(NSManagedObjectContext *)context
                  refresh:(UIRefreshControl *)refresh
{
    [NXOAuth2Request performMethod:@"GET"
                        onResource:[NSURL URLWithString:@"http://api.ip-6.nl/demos"]
                   usingParameters:nil
                       withAccount:account
               sendProgressHandler:^(unsigned long long bytesSend, unsigned long long bytesTotal) { // e.g., update a progress indicator
                   NSLog(@"Send %llu total %llu", bytesSend, bytesTotal);
               }
                   responseHandler:^(NSURLResponse *response, NSData *responseData, NSError *error){
                       NSError* err;
                       NSDictionary* json = [NSJSONSerialization
                                             JSONObjectWithData:responseData
                                             options:kNilOptions
                                             error:&err];
                       
                       NSArray* dataArray = [json objectForKey:@"demos"];
                       
                       NSMutableArray* avail = [[NSMutableArray alloc] init];
                       
                       for (NSDictionary *row in dataArray)
                       {
                           NSString *title = [row objectForKey:@"title"];
                           NSString *desc = [row objectForKey:@"description"];
                           
                           
                           NSNumber *server = [NSNumber numberWithLong:
                                               [[row objectForKey:@"id"] integerValue]];
                           
                           [avail addObject:server];
                           [self createDemo:title desc:desc serverId:server inManagedObjectContext:context];
                       }
                       
                       
                       // Delete old crap
                       NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Demo"];
                       
                       NSError *erro;
                       NSArray *matches = [context executeFetchRequest:request error:&erro];
                       
                       if ([matches count])
                       {
                           for (Demo *dm in matches)
                           {
                               if (![avail containsObject:dm.serverId])
                               {
                                   NSLog(@"Deleting %@", dm.serverId);
                                   [context deleteObject:dm];
                               }
                           }
                       }
                       if (refresh)
                       {
                           [refresh endRefreshing];
                       }
                   }];
}

Now we have a general method that does all the HTTP work, we need to have a way to call that method. Lets start with a manual refresh by the user. This can be done by dragging the TableView down. Select in the storyboard the “Demo Table View Controller”, and in the properties set “Refreshing” to enabled. Now when you pull down the list view, you will see a progress thingie. Now we just need to update our controller. First, lets assign a selector to the refreshControl:

- (void)viewDidLoad
{
    [self.refreshControl addTarget:self action:@selector(refreshView:) forControlEvents:UIControlEventValueChanged];
}

This selector will be called when we drag down. The selector itself:

- (void)refreshView:(UIRefreshControl *)refresh
{
    NSLog(@"Refresh");
    
    if (self.account && self.managedObjectContext)
    {
        [Demo getDataFromClient:self.account inManagedObjectContent:self.managedObjectContext refresh:self.refreshControl];
    }
}

Demos in the app

And we are done, and we should be able to test this now. If you have done everything right, it should look like the image in the left (With some demos :)).We of course don’t want the user to refresh himself all the time. So, why we don’t just refresh when we have a account and a managedContext? When we take a look at the code, we see that both setters call updateUI, so that seems a perfect place to update the list as well. So lets add a call to the method at the end of the if:

- (void)updateUI
{
    if (self.managedObjectContext && self.account)
    {
        self.debug = YES;
        
        
        NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Demo"];
        request.predicate = nil;
        request.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"title"
                                                                  ascending:YES
                                                                   selector:@selector(localizedStandardCompare:)]];
        
        self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
                                                                            managedObjectContext:self.managedObjectContext
                                                                              sectionNameKeyPath:nil
                                                                                       cacheName:nil];
        
        self.addButton.enabled = YES;
        
        [self.refreshControl beginRefreshing];
        [Demo getDataFromClient:self.account inManagedObjectContent:self.managedObjectContext refresh:self.refreshControl];
    }
}

Because we have the refreshControl here, we can start manual refresh that control, and include it to the getDataFromClient method. This way, the user will know we are refreshing.

Now, we of course also want to create new Demos. So lets move to the CreateNewDemoViewController. Because most of the code is already done here, we basically just need to add the code to save it to the server. So, this does all the hard work:

- (void)createDemoOnServer:(NSString *)title
                      desc:(NSString *)desc
{
    NSMutableDictionary *dt = [[NSMutableDictionary alloc]init];
    [dt setValue:title forKey:@"title"];
    [dt setValue:desc forKey:@"description"];
    
    NSData *jsonData;
    
    if ([NSJSONSerialization isValidJSONObject:dt]) {
        
        NSError *error;
        jsonData = [NSJSONSerialization dataWithJSONObject:dt options:NSJSONWritingPrettyPrinted error:&error];
        
        if (error != nil)
        {
            NSLog(@"Error creating JSON data: %@", error);
            return;
        }
    }
    else
    {
        return;
    }
    
    NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
    NSLog(@"%@", jsonString);  // To verify the jsonString.
    
    
    NXOAuth2Request *theRequest = [[NXOAuth2Request alloc] initWithResource:[NSURL URLWithString:@"http://api.ip-6.nl/demos"]
                                                                     method:@"POST"
                                                                 parameters:nil];
    theRequest.account = self.account;
    
    NSMutableURLRequest *sigendRequest = [[theRequest signedURLRequest] mutableCopy];
    
    [sigendRequest setHTTPMethod:@"POST"];
    [sigendRequest setValue:@"application/json" forHTTPHeaderField:@"Accept"];
    [sigendRequest setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
    [sigendRequest setValue:[NSString stringWithFormat:@"%lu", (unsigned long)[jsonData length]] forHTTPHeaderField:@"Content-Length"];
    [sigendRequest setHTTPBody:jsonData];
    
    
    NXOAuth2Connection *connection = [[NXOAuth2Connection alloc] initWithRequest:sigendRequest
                                                               requestParameters:nil
                                                                     oauthClient:self.account.oauthClient
                                                          sendingProgressHandler:nil
                                                                 responseHandler:^(NSURLResponse *response, NSData *responseData, NSError *error){
                                                                     NSLog(@"Done creating :) %@", error);
                                                                     
                                                                     [Demo getDataFromClient:self.account inManagedObjectContent:self.managedObjectContext refresh:nil];
                                                                     self.save.enabled = true;
                                                                     
                                                                     if (error)
                                                                     {
                                                                         // Might do something here?
                                                                         NSLog(@"ERR");
                                                                     }
                                                                     
                                                                     [self.navigationController popViewControllerAnimated:YES];
                                                                 }];
    connection.delegate = self;
}

First of, we create a dictionary with the needed items. We convert this dictionary to some JSON, that will be send. Due to our Symfony2 app works, we need to set the JSON to the POSTboy, so we create a signed request, and use that to send the data. After that, we refresh the data and are done :).
And now we just need to update createDemo to call this method:

- (IBAction)createDemo:(id)sender {
    NSLog(@"Saving...");
    
    NSString *title = self.demotitle.text;
    NSString *desc = self.demodesc.text;
    
    if ([title length] == 0 || [desc length] == 0)
    {
        if ([title length] == 0)
        {
            self.demotitle.layer.cornerRadius=8.0f;
            self.demotitle.layer.borderColor=[[UIColor redColor]CGColor];
            self.demotitle.layer.borderWidth= 1.0f;
            
            NSLog(@"Missing title");
            
        }
        if ([desc length] == 0)
        {
            self.demodesc.layer.cornerRadius=8.0f;
            self.demodesc.layer.borderColor=[[UIColor redColor]CGColor];
            self.demodesc.layer.borderWidth= 1.0f;
            
            NSLog(@"Missing desc");
        }
        
        [NSTimer scheduledTimerWithTimeInterval:2.0
                                         target:self
                                       selector:@selector(hideColor)
                                       userInfo:nil
                                        repeats:NO];
    }
    else
    {
        self.save.enabled = FALSE;
        [self createDemoOnServer:title desc:desc];
    }
}

And we are done, we can add new Demos.

The last thing we are going to do in this episode is background fetching. To do this, we need to change some of our settings. Open the settings for your app, and go to the Capabilities tab. Enable here Background modes. Once you enabled that, you get a list of items you want to be allowed. Check here “Background fetch”. It should look like the image left now.Settings
Now we have the ability to set the background fetch, we can start implementing it. First of, we need to say how often we want the fetch to happen. This is done in the didFinishLaunchingWithOptions method:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    
    self.DatabaseContext = [self createMainQueueManagedObjectContext];
    
    [application setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];
       
    return YES;
}

The completion handler should always be called, so we need to modify our method in our demo+create category to accept this. For the interface:

+ (void)getDataFromClient:(NXOAuth2Account *)account
   inManagedObjectContent:(NSManagedObjectContext *)context
                  refresh:(UIRefreshControl *)refresh
        completionHandler:(void (^)(UIBackgroundFetchResult))completionHandler;

The method itself also gets this new parameter, and we use it within the responseHandler. In the beginning of that block:

if (error)
                       {
                           completionHandler(UIBackgroundFetchResultFailed);
                           return;
                       }

And at the end:

completionHandler(UIBackgroundFetchResultNewData);

(Ofcourse, all method calls need to be updated now with the new parameter. If it isn’t a background fetch, you can just set them to nil).
Now, the last thing we need to do is implement the delegate method:

-(void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler{
    NSDate *fetchStart = [NSDate date];
    NSLog(@"Background fetch");
    
    if (!self.account || !self.DatabaseContext)
    {
        completionHandler(UIBackgroundFetchResultFailed);
    }
    
    [Demo getDataFromClient:self.account inManagedObjectContent:self.DatabaseContext refresh:nil completionHandler:^(UIBackgroundFetchResult result) {
        completionHandler(result);
        
        NSDate *fetchEnd = [NSDate date];
        NSTimeInterval timeElapsed = [fetchEnd timeIntervalSinceDate:fetchStart];
        NSLog(@"Background Fetch Duration: %f seconds", timeElapsed);
    }];  
}

Because the time is limited by iOS for this fetch to 30 seconds, it is nice to know how long it takes for each fetch. If you want to do things that take longer as 30 seconds, this background fetch is not suitable.
To test if the background fetch works, you can go to debug -> simulate background fetch.

We have nearly everything done now, in the next (and last) part in this series, we will implement editing and deleting for the iOS app.

Leave a Reply

Your email address will not be published. Required fields are marked *