Skip to content
Jon Gilkison edited this page Nov 12, 2012 · 4 revisions

ModelKit was originally designed to replace Parse.com's iOS SDK. Therefore, a primary goal of this framework is not only model persistence/management, but tying those models to a backend, either a BaaS like Parse or Kinvey, or your own custom REST solution.

Currently, ModelKit supports Parse through their REST API.

###Setting Up The Service Setting up the service is pretty simple. In the code for your application launch:

	[MKitServiceManager setupService:@"Parse" withKeys:@{@"AppID":PARSE_APP_ID,@"RestKey":PARSE_REST_KEY}];

###Making Your Models Usable With The Service To use your models with Parse, simply make them subclasses of MKitParseModel:

	@interface CSMPAuthor : MKitParseModel
    
    @property (copy, nonatomic) NSString *name;
    @property (copy, nonatomic) NSString *email;
    @property (retain, nonatomic) NSDate *birthday;
    @property (assign, nonatomic) NSInteger age;
    @property (assign, nonatomic) BOOL displayEmail;
    @property (copy, nonatomic) NSString *avatarURL;
    
    @end

If you were to use this straight away, you'd notice in the Parse data browser that your classes are named CSMPAuthor. If this is not what you want, you can change this behavior by doing the following in your implementation:

	@implementation CSMPAuthor
	
	+(void)load
	{
	    [self register];
	}
	
	+(NSString *)modelName
	{
	    return @"Author";
	}
	
	@end

Two things are happening here. When you override +(NSString *)modelName you are telling the system the name of the class you want to use for Parse. With this overridden, after you save your model, in the Parse data browser you would then see a class Author instead of CSMPAuthor.

If you do override +(NSString *)modelName, you'll also need to override +(void)load and call [self register]; to make sure that your custom model name is registered with your model class so that when data returned from Parse is -well- parsed, that the correct model types are created.

###Saving and Updating Saving models to Parse is as simple as can be. You have two choices, synchronous and asynchronous. Let's look at synchronous first:

	-(void)someAction:(id)sender
	{
		CSMPAuthor *author=[CSMPAuthor instance];
		author.email=@"jon@interfacelab.com";
	    author.name=@"Jon Gilkison";
	    author.age=39;
	    author.displayEmail=YES;
	    author.avatarURL=@"https://secure.gravatar.com/avatar/819b112459b48de1e3ea08128b8e5479";
	    
	    NSError *error=nil;
	    if (![author save:&error])
	    	NSLog(@"Oops: %@",error);
	    else
	    	NSLog(@"Saved!");
	}

That was easy, but there is a problem. Synchronous saving will block the thread while it waits for the request to be sent to Parse and then wait for Parse to send a response. This is fine if you're running this in a background thread, but in your main thread, this will block your UI and make your app unresponsive.

Asynchronous is better:

	-(void)someAction:(id)sender
	{
	    CSMPAuthor *author=[CSMPAuthor instance];
	    author.email=@"jon@interfacelab.com";
	    author.name=@"Jon Gilkison";
	    author.age=39;
	    author.displayEmail=YES;
	    author.avatarURL=@"https://secure.gravatar.com/avatar/819b112459b48de1e3ea08128b8e5479";
	    
	    [author saveInBackground:^(BOOL succeeded, NSError *error) {
	       if (!succeeded)
	           NSLog(@"OOPS %@",error);
	        else
	            NSLog(@"Saved!");
	    }];
	}

The save will now happen in the background and your main thread can keep on trucking.

What about updating? The save method creates and updates automatically, so you'll call it whether you are creating a new object or updating an existing one.

One thing to note is that if your model has model properties or a property that's an array of models, those models will automatically be saved or updated if needed, no need to do it one by one yourself.

###Deleting This works the same way as saving. Synchronously:

	-(void)someAction:(id)sender
	{
		CSMPAuthor *author=[CSMPAuthor instanceWithObjectId:@"as322DdddzJ"];
	    
	    NSError *error=nil;
		if (![author delete:&error])
	    	NSLog(@"Oops: %@",error);
	    else
	    	NSLog(@"Deleted!");
	}

Asynchronously:

	-(void)someAction:(id)sender
	{
		CSMPAuthor *author=[CSMPAuthor instanceWithObjectId:@"as322DdddzJ"];
	    
	    [author deleteInBackground:^(BOOL succeeded, NSError *error) {
	       if (!succeeded)
	           NSLog(@"OOPS %@",error);
	        else
	            NSLog(@"Deleted!");
	    }];
	}

###Fetching There will be instances where models don't have all their data from Parse. This could be the result from a query or by creating an instance with a known parse objectId. To know if you need to fetch this data, you can inspect the modelState property and if it's equal to ModelStateNeedsData then that is a sure sign you need to get to fetching.

And just like saving and deleting:

	-(void)someAction:(id)sender
	{
		CSMPAuthor *author=[CSMPAuthor instanceWithObjectId:@"as322DdddzJ"];
	    
	    if (author.modelState==ModelStateNeedsData)
	    {
	    	NSError *error=nil;
			if (![author fetch:&error])
	    		NSLog(@"Oops: %@",error);
	    	else
	    		NSLog(@"fetched!");
	    }
	}

Asynchronously:

	-(void)someAction:(id)sender
	{
		CSMPAuthor *author=[CSMPAuthor instanceWithObjectId:@"as322DdddzJ"];
	    
	    if (author.modelState==ModelStateNeedsData)
	    {
		    [author fetchInBackground:^(BOOL succeeded, NSError *error) {
		       if (!succeeded)
		           NSLog(@"OOPS %@",error);
		        else
		            NSLog(@"Fetched!");
		    }];
		}
	}

###Querying Querying works exactly the same as described in the section above, except that instead of using NSPredicates to query the graph, you are generating web requests to Parse. Otherwise, the interface and usage is exactly the same.

But what if you want to query the graph? Maybe your user doesn't have an internet connection or you just want to search locally. MKServiceModel, which MKParseModel subclasses, has a static class method called graphQuery. Using this will query the graph instead of the backend service:

	-(void)someAction:(id)sender
	{
		MKitModelQuery *query=[CSMPAuthor graphQuery];
		
		// Let's find all authors between the ages of 24 to 35
		[query key:@"age" condition:KeyBetween value:@[@(24),@(35)]];
		
		NSDictionary *result=[query execute:nil];
		NSArray *found=[result objectForKey:MKitQueryResultKey];
		for(CSMPAuthor *author in found)
			NSLog(@"Author %@ is %d",author.name,author.age);
	}

###Users

ModelKit provides a working User model out of the box. Note the user only works with a backend service because it wouldn't make any sense if your data was always local. Duh.

If you are familiar with PFUser from the Parse iOS SDK, it works almost identically, except that you can subclass it and add your own properties to it.

The Parse user class is MKitParseUser, which is a subclass of MKitParseModel that implements the MKitServiceUser protocol. Because it's a model, everything you can do with that you can do with MKitParseModel.

####Determining If The Current User Is Logged In You can check to see if the current user is logged in by calling the currentUser static method:

	if ([MKitParseUser currentUser])
	{
		// Logged In!
	}

####Signing Up Signing up your users for your application is straight forward. As with saving/deleting/fetching, it can be done synchronously or asynchronously depending on your needs. Synchronously:

	-(void)signUpTouched:(id)sender
	{
	    NSError *error=nil;
	    BOOL result=[MKitParseUser signUpWithUserName:@"bob" email:@"bob@somewhere.com" password:@"password" error:&error];
	    if (!result)
	        NSLog(@"ERROR: %@",error);
    }

Asynchronously:

	-(void)signUpTouched:(id)sender
	{
		[MKitParseUser signUpInBackgroundWithUserName:@"bob"
		                                            email:@"bob@somewhere.com""
		                                         password:@"password"
		                                      resultBlock:^(id object, NSError *error) {
		                                          if (error)
		                                              NSLog(@"ERROR: %@", error);
		                                      }];
	}

Once the user is signed up, that user will be logged in. There is no need to log them in.

####Logging In

	-(void)signUpTouched:(id)sender
	{
	    [MKitParseUser logInInBackgroundWithUserName:@"bob"
	                                         password:@"password"
	                                      resultBlock:^(id object, NSError *error) {
	                                         if (error)
	                                        	  NSLog(@"ERROR: %@",error);
	                                      }];
	}

Logging in will keep the user logged in forever, the user session never expires.

####Logging Out

	-(void)logOutTouched:(id)sender
	{
		[[MKitParseUser currentUser] logOut];
	}

####Forgotten Password?

	-(void)forgotPasswordTouched:(id)sender
	{
	    [MKitParseUser requestPasswordResetInBackgroundForEmail:@"bob@somewhere.com"
                                                    resultBlock:^(BOOL succeeded, NSError *error) {
                                                        if (error)
                                                            NSLog(@"ERROR: %@",error);
                                           }];

	}

###Files

ModelKit also provides a mechanism to upload files to your backend and associating those files with your users. Like users, this feature requires a backend integration and is not available for local models.

MKitParseFile subclasses MKitServiceFile, but it is important to note that it is not a subclass of any model and should not be treated like a model. In other words, you cannot add properties to it. But what if that is what you want to do? For example, let's say you want to model a Photograph. You would create a Photograph model and have MKitParseFile as property of that model that points to the file. Read more about this below because there are some important caveats.

####Uploading Files Uploading files is very simple and supports synchronous and asynchronous operation. I will only demonstrate asynchronous:

    MKitParseFile *p=[MKitParseFile fileWithFile:[[NSBundle mainBundle] pathForResource:@"image2" ofType:@"jpeg"]];
    [p saveInBackgroundWithProgress:^(float progress) {
        NSLog(@"%f",progress);
    }
                        resultBlock:^(BOOL succeeded, NSError *error) {
                            if (succeeded)
                                NSLog(@"%@",p.url);
                            else
                                NSLog(@"ERORR: %@",error);
                        }];

You are not limited to uploading an existing file, you can upload an NSData instance as well. This is a convoluted example, but you get the point:

    NSData *data=[NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"image2" ofType:@"jpeg"]
                                        options:NSDataReadingMappedAlways
                                          error:nil];
    MKitParseFile *p=[MKitParseFile fileWithData:data name:@"image2.jpeg" contentType:@"image/jpeg"];
    [p saveInBackgroundWithProgress:^(float progress) {
        NSLog(@"%f",progress);
    }
                        resultBlock:^(BOOL succeeded, NSError *error) {
                            if (succeeded)
                                NSLog(@"%@",p.url);
                            else
                                NSLog(@"ERORR: %@",error);
                        }];

Once the upload has successfully completed, the url and name properties will be populated with values returned from the server. url will be a valid URL to the uploaded resource and name will contain the backend's unique name for the file.

####Uploading Batches ModelKit provides the MKitMutableFileArray class which allows you to easily do batch uploading (and is useful as an array of files property on a model). It's a subclass of NSMutableArray so you can cast it up the chain no problem. Here's an example of a batch upload:

    MKitMutableFileArray *files=[MKitMutableFileArray array];
    
    // Add 10 files to the array to upload
    for(int i=0; i<10; i++)
        [files addObject:[MKitParseFile fileWithFile:[[NSBundle mainBundle] pathForResource:@"image2" ofType:@"jpeg"]]];
    
    [files uploadInBackgroundWithProgress:^(NSInteger current, NSInteger total, float currentProgress, float totalProgress) {
        NSLog(@"File %d of %d - Progress %f - Total Progress %f",current,total, currentProgress, totalProgress);
    } resultBlock:^(BOOL succeeded, NSError *error) {
        NSLog(@"Finished");
    }];

####Associating With Models

You can use MKitParseFile as a property of an model, but there are some things you must know:

  • You must retain the property, not assign as you would do for a model property
  • The file must be uploaded to the backend before saving your model to the backend. Failure to do so will raise an exception.

With those two things in mind, here's a model with a file as a property:

	#import "ModelKit.h"
    
    @interface CSMPPhoto : MKitParseModel
    
    @property (assign, nonatomic) MKitParseUser *owner;
	@property (retain, nonatomic) MKitParseFile *photo;
	    
    @end

####Deleting Files Like models, you can simply call delete on the file to delete it. Unless you are using Parse. If you are using Parse, Parse requires you to use the MasterKey to delete files from the backend. Therefore, it is not possible to delete them with ModelKit.

However, if you delete a model that has a file as a property, you can clean out "unused" files in the Parse data dashboard.

###Calling Backend Code TBD

###Offline and Syncing TBD

Clone this wiki locally