oAuth2 with iOS (part 2)

Within this post we continue working on the iOS app. For previous posts in this series, see here :).
Apple announced the second BETA for iOS8 last tuesday, and due to that, the devices I use for this example are upgraded to this second BETA.

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

So, what are we going to do in this second part? First, we are going to create some basic functionality in the app to add a new demo. Second, we are going to write some tests, including some performance tests, to make sure everything works, even after we are modifying it to work with our network code (Which will be later in a next part). Creating tests for apps (And other code as well of course) is always a great idea. With the next xCode feature apple introduces in iOS8/xCode6 you can do lots of great stuff. I am going to use some of these features to do testing of this demo app.
In a other post, which is not within this series, but still interesting, I will explain how to use xCode server for continues integration with iOS8 and github, because that is (badly enough) not as straight forward as it can be.

Creating a new item in a core data model isn’t very difficult. Because we overwrite the Demo class anytime we generate it, that is not a good location to add it, isn’t it? well, it actually is, but we are adding a category for that demo class, and use that instead.
So lets create a new Cacao class, and name it Demo+Create. Last time, we had a problem creating a new category. This issue doesn’t seem to be fixed yet in the second BETA, so we need to do it by hand.

Create a Demo+create.h:

//
//  Demo+Create.h
//  Symfony iOS Oauth Demo
//
//  Created by Paul Sohier on 20-06-14.
//  Copyright (c) 2014 Paul Sohier. All rights reserved.
//

#import 
#import "demo.h"


@interface Demo (Create)

@end

And a demo+Create.m:

//
//  Demo+Create.m
//  Symfony iOS Oauth Demo
//
//  Created by Paul Sohier on 20-06-14.
//  Copyright (c) 2014 Paul Sohier. All rights reserved.
//

#import "Demo+Create.h"

@implementation Demo (Create)

@end

We now have a new Category on demo. Even if we replace our Demo.m/h, we still will have our newly created files with the code to add new Demo’s.

So, lets create the interface for our demo in the .h file. Besides creating a method for creating a item, we also directly creating the interface for deleting which we add later on :).

+ (Demo *)createDemo:(NSString *)title
                desc:(NSString *)desc
            serverId:(NSNumber *)serverId
inManagedObjectContext:(NSManagedObjectContext *)context;

+ (void)deleteDemo:(NSNumber *)serverId
inManagedObjectContext:(NSManagedObjectContext *)context;

To create a demo, we need both the title and description, which are shown to the user in our interface. We also need the serverId, which we use to match items from the server with our local item.
Because we also need to insert something, we need a context. We can’t just listen to the observer, because that is send when our app starts. The easiest way to get it is just to have it is a parameter.

The implementation is as following:

+ (Demo *)findDemo:(NSNumber *)serverId
inManagedObjectContext:(NSManagedObjectContext *)context
{
    Demo *demo = nil;
    
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Demo"];
    request.predicate = [NSPredicate predicateWithFormat:@"serverId = %@", serverId];
    
    NSError *erro;
    NSArray *matches = [context executeFetchRequest:request error:&erro];
    
    if (matches)
    {
        demo = [matches firstObject];
    }
    
    return demo;
}

+ (Demo *)createDemo:(NSString *)title
                desc:(NSString *)desc
            serverId:(NSNumber *)serverId
inManagedObjectContext:(NSManagedObjectContext *)context
{
    Demo *demo = nil;
    if ([title length] && [desc length])
    {
        demo = [self findDemo:serverId inManagedObjectContext:context];
        
        if (demo == nil)
        {
            demo = [NSEntityDescription insertNewObjectForEntityForName:@"Demo" inManagedObjectContext:context];
        }
        
        demo.title = title;
        demo.desc = desc;
        demo.serverId = serverId;

        NSError *err;
        
        [context save:&err];
        
        if (err)
        {
            NSLog(@"Err %@ %@", err.localizedDescription, err.description);
        }
    }
    else
    {
        NSLog((@"Missing title or desc."));
    }
    return demo;
}

When the createDemo doesn’t receive a title or description, we log that and return nil afterwards. Both are in all cases required to be filled in.
If we have both the title as the description, we are going to check if there is already a item with this serverId in the database. When there is, we use that object to update the data. If there isn’t, we create a brand new object instead. Because we want to find a demo in our delete method later on as well, we created a method for just that.

Before we get to the tests, lets create the method for deleting first:

+ (void)deleteDemo:(NSNumber *)serverId
inManagedObjectContext:(NSManagedObjectContext *)context
{
    Demo *demo = [self findDemo:serverId inManagedObjectContext:context];
    
    [context deleteObject:demo];
}

We don’t do much special here, besides just calling deleteObject on the context.

Creating a test with iOS is pretty simple. When you created your project, Xcode already created a test file. You can modify this file (In my case called Symfony_iOS_Oauth_DemoTests.m in the Symfony iOS Oauth DemoTests directory), and add new tests to it by creating a method that starts with test, followed by a name or description.
But first, we need to create something that gives us a context for our Core Data. We don’t want to use the one on a device (If we use a device to test) where already data is from normal usage. Instead, we want to create a temporary, in memory, context from our model.
So, lets add a method to our test to create the context:

- (NSManagedObjectContext *)managedObjectContextForTesting {
    NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] init];
    
    NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"Model" withExtension:@"momd"];
    NSManagedObjectModel *mom = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
    
    NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom];
    
    [psc addPersistentStoreWithType:NSInMemoryStoreType configuration:nil URL:nil options:nil error:nil];
    
    [moc setPersistentStoreCoordinator:psc];
    
    return moc;
}

We create a field in our interface for the context property:

@property (strong, nonatomic) NSManagedObjectContext *context;

And initialise it in setUp:

self.context = [self managedObjectContextForTesting];

Now we can use this to do our core data stuff :).

To do our tests we want to compare whats in the database with the object that we created. So we need something to get a demo from our database:

- (Demo *)getDemo:(NSNumber *)serverId
          context:(NSManagedObjectContext *)context
{
    return [self getDemo:serverId testReturn:YES context:context];
}
- (Demo *)getDemo:(NSNumber*)serverId
       testReturn:(BOOL)testReturn
          context:(NSManagedObjectContext *)context
{
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Demo"];
    request.predicate = [NSPredicate predicateWithFormat:@"serverId = %@", serverId];
    
    NSError *erro;
    NSArray *matches = [context executeFetchRequest:request error:&erro];
    
    if (testReturn)
    {
        XCTAssertTrue([matches count] == 1);
    }
    
    return [matches firstObject];
}

Within this method, if testReturn is YES, we assert that matches is always 1. Because we use this method later on too test if something is deleted, we want to have this as option.

Now we have the helper methods, we can create a first test case. Lets start with creation:

- (void)testDemoCreate
{
    NSString *title = @"title";
    NSString *desc = @"desc";
    NSNumber *serverID = [NSNumber numberWithInt:arc4random()];
    
    Demo *demo = [Demo createDemo:title desc:desc serverId:serverID inManagedObjectContext:self.context];
    XCTAssertTrue([title isEqualToString:demo.title]);
    XCTAssertTrue([desc isEqualToString:demo.desc]);
    
    XCTAssertEqual(serverID.intValue, demo.serverId.intValue);
    
    Demo *dm = [self getDemo:serverID context:self.context];
    
    XCTAssertTrue([title isEqualToString:dm.title]);
    XCTAssertTrue([desc isEqualToString:dm.desc]);
    XCTAssertEqual(serverID.intValue, dm.serverId.intValue);
    
    title = @"Title2";
    desc = @"Desc2";
    
    demo.title = title;
    demo.desc = desc;
    
    Demo *dm2 = [self getDemo:serverID context:self.context];
    
    XCTAssertTrue([title isEqualToString:dm2.title]);
    XCTAssertTrue([desc isEqualToString:dm2.desc]);
    XCTAssertEqual(serverID.intValue, dm2.serverId.intValue);
}

Within this test we first insert a new demo, checks if the returned results is equal to the provided info, and after that we select the item from the database, to make sure it is in there as well and is correct. After that we modify the item, and check if the modified item is saved correctly in the database as well.

Now lets test deletion:

- (void)testDemoDelete
{
    NSString *title = @"title";
    NSString *desc = @"desc";
    NSNumber *serverID = [NSNumber numberWithInt:arc4random()];
    
    Demo *demo = [Demo createDemo:title desc:desc serverId:serverID inManagedObjectContext:self.context];
    
    demo = [self getDemo:serverID context:self.context];
    
    [Demo deleteDemo:serverID inManagedObjectContext:self.context];
    
    demo = [self getDemo:serverID testReturn:NO context:self.context];
    
    XCTAssertTrue(demo == nil);
}

First, we create again a new demo. Without a demo we can’t of course delete a demo.
After that, we delete it via deleteDemo, and check via getDemo if it is really deleted.

Now we can create new demo’s (And delete them of course), it is time to implement this into the app itself. In a later part of this series, we will modify this part of the code again, so it is send to the server instead, but now we want to make sure first everything works.
To handle our previously created form to create a demo we need a new view controller. This can be created via file -> new -> Cocoa Class. Name it a CreateDemoViewController and extend from UIViewController.
We now need to assign the view controller to the view. Assign view controllerAt the top right as shown in the image left we can assign the view controller.
Now, we want to create actions when the user clicks on save. To do this, we need to ctrl drag that from the view into the code. We also need to be able to read the title and description fields. These can also be ctrl dragged into the code. After you did this, it should look like the image on the left. Notice the small icons in front of the lines for the method and the properties.
outlets
Because we need to use the context to save data to the database, we should provide this when we segue to the new controller. First, add the property to the interface in the .h file:

@property (nonatomic, strong) NSManagedObjectContext *managedObjectContext;

Now, in the demoTableViewController we can set this value in prepareForSegue:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([[segue identifier] isEqualToString:@"create-new-demo"])
    {
        CreateDemoViewController *vw = (CreateDemoViewController *) segue.destinationViewController;
        vw.managedObjectContext = self.managedObjectContext;
    }
}

We also will need to import (of course) right header for this to work.

Now we have everything we need, we can implement the inaction for createDemo:

- (IBAction)createDemo:(id)sender {
    NSLog(@"Saving...");
    
    NSString *title = self.demotitle.text;
    NSString *desc = self.description.text;
    NSNumber *serverId = [NSNumber numberWithInt:arc4random()];
    
    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.description.layer.cornerRadius=8.0f;
            self.description.layer.borderColor=[[UIColor redColor]CGColor];
            self.description.layer.borderWidth= 1.0f;
            
            NSLog(@"Missing desc");
        }
        
        [NSTimer scheduledTimerWithTimeInterval:2.0
                                         target:self
                                       selector:@selector(hideColor)
                                       userInfo:nil
                                        repeats:NO];
    }
    else
    {
        [Demo createDemo:title desc:desc serverId:serverId inManagedObjectContext:self.managedObjectContext];
        
        NSLog(@"Created demo with serverId %@", serverId);
        
        [self.navigationController popViewControllerAnimated:YES];
    }
}

First, we check if the title and description are filled in. If they aren’t, we make the border red. If they are filled in nicely, we call the createDemo method in the category we created earlier. After that, we return back to the original controller in our code. In normal apps, you might want to inform the user in a nicer way that the demo has been created.

If something is filled in, we create temporary a red border. We need to restore that back after to seconds. When that happens a method hideColor is called by NSTimer. This method restores the border:

- (void)hideColor
{
    self.demotitle.layer.borderColor=[[UIColor clearColor]CGColor];
    self.demotitle.layer.cornerRadius = 0;
    
    self.description.layer.borderColor=[[UIColor clearColor]CGColor];
    self.description.layer.cornerRadius = 0;
}

Error, fields not filled in
To use layer for the UITextField we also need to import QuartzCore/QuartzCore.h. Because we use createDemo, we also need to include the category we created.

So lets do some testing. When you go to the field and don’t fill anything it should look like the image left. After you filled in something, and get back to the main window, you will see them displayed in the tableview as shown in the second image.
Lastly, we want sometimes to delete a demo as well. Luckily iOS provided interfaces to make this very easy. So lets implement that.
To implement deleting by swipe to the left, you simply implement the following method from the UITableViewDataSource protocol.
table view
If you read the documentation, it is already mentioned in the top of the document:

Note: To enable the swipe-to-delete feature of table views (wherein a user swipes horizontally across a row to display a Delete button), you must implement the tableView:commitEditingStyle:forRowAtIndexPath: method.

So lets implement that method. Because we are already implement UITAbleViewDataSource in our CoreDataViewController we don’t need to do anything related to that, and just add the new method. Because we are going to use the category on Demo, we need to include demo+create.h (Which has a confusing name by now, hasn’t it?). After that, we can just call deleteDemo with the right serverId:

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSLog(@"Delete");
    Demo *demo = [self.fetchedResultsController objectAtIndexPath:indexPath];
    NSNumber *serverId = demo.serverId;
    [Demo deleteDemo:serverId inManagedObjectContext: self.managedObjectContext];
}

deleteDemo expects a serverId as parameter, so we need to get the Demo based on the forRowAtIndexPath parameter with objectAtIndexPath. Once we have the serverId, we can call the deleteDemo.

Now we have everything implemented to have a functional app. In the next part in this series we will implement the oAuth part in our iOS app, and finish everything.

Leave a Reply

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