The RhoConnect Client is a library that adds sync data capability to your non-Rhodes applications, such as applications created in XCode or Android Java. Using this library, you can enable synchronized data between your Objective C iOS device application and a RhoConnect server.
The source code for the RhoConnect client store example is located here.
Feature | Ruby | ObjectiveC |
---|---|---|
Property bag model | yes | yes |
Fixed schema mode | yes | yes |
Model associations | yes | yes |
BLOB attributes | yes | yes |
Bulk sync | yes | yes |
Sync | yes | yes |
Notifications | yes | yes |
Database reset | yes | yes |
Authentication | yes | yes |
DB partitioning | yes | not yet |
The RhoConnect Client is part of the Rhoconnect-client repository; it contains files that you will need to add to your Xcode project. You can get the client on github. (Select the latest ‘rhoconnect-client’ package).
Create a new Xcode project or open an existing one in Xcode.
Do the following steps to add <rhoconnect-client>/objectivec/RhoConnectClient.xcodeproj
as a dependency so that the RhoConnectClient library will link with your project.
<rhoconnect-client>/ObjectiveC/RhoConnectClient.xcodeproj
from the Finder into Groups & Files for your Xcode project. RhoConnectClient.xcodeproj should now appear in Groups & Files.Do the following steps to add libraries:
libstdc++6.dylib, libsqlite3.dylib, libz.dylib, CFNetwork.framework
.Do the following steps to add schema files to your Copy Bundle Resources build phase:
1. Copy the folder <rhodes>/platform/shared/db/res/db
into your Xcode project folder.
2. Drag the copied db folder into your Xcode project, onto the project-named icon under Groups & Files. The db folder should now appear in the Groups & Files list.
3. Under Groups and Files, open the Targets icon, then open the project-named icon under Targets, then open the Copy Bundle Resources folder. You should see sync.schema listed there.
4. Open and edit the sync.schema file. Add a create table statement for every model you will use in your project. For example, for a model named product:
CREATE TABLE product ( "client_id" VARCHAR(255) default NULL);
Do not use NOT NULL statement in column definitions. RhoConnect client delete object attributes by setting value to null, when all object attributes become nulls, object deleted. So objects with NOT NULL columns will not be deleted.
This section discusses the general steps you perform to code a RhoConnect client in Objective C, using the store example. It is not a tutorial for coding a complete Xcode application, but instead discusses the RhoConnect code that you need to create.
Call the RhoConnectClient method initDatabase to initialize the local database for the models. You must do this before you initialize and use a RhoConnectClient object.
[RhoConnectClient initDatabase];
Call the RhomModel init method for each model. In the Store example, initialize a customer model and a product model.
customer = [[RhomModel alloc] init]; customer.name = @"Customer"; product = [[RhomModel alloc] init]; product.name = @"Product";
Call init for RhoConnectClient to initialize your client.
sclient = [[RhoConnectClient alloc] init];
Set up an NSMutableArray containing the models for your client.
NSMutableArray* models = [NSMutableArray arrayWithObjects:customer, product, nil];
Call the RhoConnectClient method addModels to add those models to your client.
[sclient addModels:models];
Set the RhoConnectClient property sync_server to the URL of the RhoConnect server for your client.
sclient.sync_server = @"http://rhodes-store-server.heroku.com/application";
Here is the Store code for the initialization process, contained in RhoConnectEngine.m.
- (id)init { if ( sharedInst != nil ) { [NSException raise:NSInternalInconsistencyException format:@"[%@ %@] cannot be called; use +[%@ %@] instead", NSStringFromClass([self class]), NSStringFromSelector(_cmd), NSStringFromClass([self class]), NSStringFromSelector(@selector(sharedInstance))]; } else if ( self = [super init] ) { sharedInst = self; [RhoConnectClient initDatabase]; customer = [[RhomModel alloc] init]; customer.name = @"Customer"; product = [[RhomModel alloc] init]; product.name = @"Product"; sclient = [[RhoConnectClient alloc] init]; NSArray* models = [NSArray arrayWithObjects:customer, product, nil]; [sclient addModels:models]; sclient.sync_server = @"http://rhodes-store-server.heroku.com/application"; sclient.threaded_mode = TRUE; sclient.log_severity = 1; loginState = [sclient is_logged_in] ? logged_in : logged_out; } return sharedInst; }
Call the RhoConnectClient loginWithUser method to log into the RhoConnect server.
[ [RhoConnectEngine sharedInstance].syncClient loginWithUser:txtLogin.text pwd:txtPassword.text callback:@selector(loginComplete:) target:self];
Here is the Store code for the login process, contained in LoginViewController.m. It contains both a login method that calls loginWithUser, and a callback method called by loginWithUser, that checks to see if the login is active.
- (void)loginComplete:(RhoConnectNotify*) notify { if ( notify.error_code != RHO_ERR_NONE || ![[RhoConnectEngine sharedInstance].syncClient is_logged_in]) [RhoConnectEngine sharedInstance].loginState = failed; else [RhoConnectEngine sharedInstance].loginState = logged_in; [waitPage loginComplete: [notify error_message] ]; [notify release]; } - (IBAction)doLogin:(id)sender { [RhoConnectEngine sharedInstance].loginState = in_progress; [[self navigationController] pushViewController:waitPage animated:YES]; [ [RhoConnectEngine sharedInstance].syncClient loginWithUser:txtLogin.text pwd:txtPassword.text callback:@selector(loginComplete:) target:self]; }
Call the RhoConnectClient setNotification method to call a callback method that notifies about the state of the synchronization.
[ [RhoConnectEngine sharedInstance].syncClient setNotification: @selector(syncAllComplete:) target:self];
To perform a bulk sync bulksync_state configuration flag should be reset to zero. If you don’t need bulk sync then just skip it.
// set configuration for bulk sync, it means "next sync should be performed as bulk" [RhoConnectEngine sharedInstance].syncClient.bulksync_state = 0; // after sync performed the it should contain value 1.
Call a sync method, such as the RhoConnectClient syncAll method or the RhomModel sync method, to sync your client model data with the RhoConnect server.
[ [RhoConnectEngine sharedInstance].syncClient syncAll];
Call clearNotification upon completion of the sync.
[ [RhoConnectEngine sharedInstance].syncClient clearNotification];
Here is the Store code for the sync process, contained in WaitLoginController.m. It contains both a loginComplete method (called from the loginWithUser callback method) that calls setNotification and syncAll, and the setNotification callback method that calls clearNotification when the sync is complete.
- (void)syncAllComplete:(RhoConnectNotify*) notify { if ( [notify.status compare:@"in_progress"] == 0) { }else if ([notify.status compare:@"complete"] == 0) { [[self navigationController] pushViewController:homePage animated:YES]; [ [RhoConnectEngine sharedInstance].syncClient clearNotification]; }else if ([notify.status compare:@"error"] == 0) { if([notify.error_message caseInsensitiveCompare:@"unknown client"] == 0) { [[RhoConnectEngine sharedInstance].syncClient database_client_reset]; [[RhoConnectEngine sharedInstance].syncClient setNotification: @selector(syncAllCalback:) target:self]; [[RhoConnectEngine sharedInstance].syncClient syncAll]; } else if( err_code == RHO_ERR_CLIENTISNOTLOGGEDIN || err_code == RHO_ERR_UNATHORIZED) { NSLog(@"GO TO LOGIN PAGE!"); // real code to trigger view transition goes here.. }else { //This is mandatory: if([notify hasCreateErrors]) { [[RhoConnectEngine sharedInstance].syncClient onCreateError: notify @"delete"]; } //These are optional: /* if([notify hasUpdateErrors]) { [[RhoConnectEngine sharedInstance].syncClient onUpdateError: notify @"rollback"]; } if([notify hasDeleteErrors]) { [[RhoConnectEngine sharedInstance].syncClient onDeleteError: notify @"retry"]; } */ } } } - (void)loginComplete:(NSString*) errorMessage { NSLog(@"Login error message: \"%@\"", errorMessage); [indicator stopAnimating]; if ([RhoConnectEngine sharedInstance].loginState == logged_in) { [ [RhoConnectEngine sharedInstance].syncClient setNotification: @selector(syncAllComplete:) target:self]; [ [RhoConnectEngine sharedInstance].syncClient syncAll]; } else { lblMessage.text = errorMessage; self.navigationItem.hidesBackButton = false; } }
Unknown client error return by server after resetting server database, removing particular client id from database or any other cases when server cannot find client id(sync server unique id of device). Note that login session may still exist on server, so in this case client does not have to login again, just create new client id. Processing of this error contain 2 steps:
When unknown client error is come from server, client should call database_client_reset and start new sync, to register new client id.
If login session also deleted or expired on the server, then customer has to login again.
Example: :::cplusplus
- (void)syncAllComplete:(RhoConnectNotify*) notify { NSString* status = notify.status; NSString* error = notify.error_message; int err_code = notify.error_code; NSLog(@"syncAll DONE, status: '%s' , error_msg: '%s' , error_code: %d", [status cStringUsingEncoding: NSUTF8StringEncoding], [error cStringUsingEncoding: NSUTF8StringEncoding], err_code ); if ( [notify.status compare:@"in_progress"] == 0) { } else if ([notify.status compare:@"complete"] == 0) { [[RhoConnectEngine sharedInstance].syncClient clearNotification]; } else if ([notify.status compare:@"error"] == 0) { if([notify.error_message caseInsensitiveCompare:@"unknown client"] == 0) { [[RhoConnectEngine sharedInstance].syncClient database_client_reset]; [[RhoConnectEngine sharedInstance].syncClient setNotification: @selector(syncAllCalback:) target:self]; [[RhoConnectEngine sharedInstance].syncClient syncAll]; } else if( err_code == RHO_ERR_CLIENTISNOTLOGGEDIN || err_code == RHO_ERR_UNATHORIZED) { NSLog(@"GO TO LOGIN PAGE!"); // real code to trigger view transition goes here.. } } }
create-error: has to be handled in sync callback. Otherwise sync will stop on this model. To fix create errors you should call Model.on_sync_create_error or SyncEngine.on_sync_create_error:
update-error: If not handled, local modifications, which were failing on server, will never sync to server again. So sync will work fine, but nobody will know about these changes.
delete-error: If not handled, local modifications, which were failing on server, will never sync to server again. So sync will work fine, but nobody will know about these changes.
Example: :::cplusplus
- (void)syncAllCalback:(RhoConnectNotify*) notify { NSString* status = notify.status; NSString* error = notify.error_message; int err_code = notify.error_code; NSLog(@"syncAll DONE, status: '%s' , error_msg: '%s' , error_code: %d", [status cStringUsingEncoding: NSUTF8StringEncoding], [error cStringUsingEncoding: NSUTF8StringEncoding], err_code ); if ( [notify.status compare:@"in_progress"] == 0) { } else if ([notify.status compare:@"complete"] == 0) { [[RhoConnectEngine sharedInstance].syncClient clearNotification]; } else if ([notify.status compare:@"error"] == 0) { if([notify isUnknownClientError]) { [[RhoConnectEngine sharedInstance].syncClient database_client_reset]; [[RhoConnectEngine sharedInstance].syncClient setNotification: @selector(syncAllCalback:) target:self]; [[RhoConnectEngine sharedInstance].syncClient syncAll]; } else if( err_code == RHO_ERR_CLIENTISNOTLOGGEDIN || err_code == RHO_ERR_UNATHORIZED) { NSLog(@"GO TO LOGIN PAGE!"); // real code to trigger view transition goes here.. }else { //This is mandatory: if([notify hasCreateErrors]) { [[RhoConnectEngine sharedInstance].syncClient onCreateError: notify action:@"delete"]; } //These are optional: /* if([notify hasUpdateErrors]) { [[RhoConnectEngine sharedInstance].syncClient onUpdateError: notify action:@"rollback"]; } if([notify hasDeleteErrors]) { [[RhoConnectEngine sharedInstance].syncClient onDeleteError: notify action:@"retry"]; } */ } } }
Call a RhomModel find method (find, find_first, find_all) to find model objects in the client database. This code sample for the find_all method fetches all the models in the database; you can also search for models containing certain attribute data.
NSMutableArray* arItems = [[RhoConnectEngine sharedInstance].product find_all:nil]; NSDictionary* item = [arItems objectAtIndex: 0 ]; textBrand = [item valueForKey:@"brand"];
Here is the Store code for the find process, contained in RootViewController.m. It contains a call to the find_all method, and returns the total count of the models in the database.
// Customize the number of rows in the table view. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if (!arItems) arItems = [[RhoConnectEngine sharedInstance].product find_all:nil]; //warning here because this may be dictionary or an array when called. return (NSUInteger)[arItems count]; }
The RhoConnectClient class contains the following properties and methods to build an Objective C RhoConnect client and connect it with a RhoConnect server.
BOOL. Set to false to get the result of operations as a return value. (Set to true to get notifications - not supported yet.) Default = false.
@property(setter=setThreadedMode) BOOL threaded_mode;
int. Not yet supported. Default = 0.
@property(setter=setPollInterval) int poll_interval;
int. Set minimal severity level for messages to log output. Default = 0.
@property(setter=setLogSeverity) int log_severity;
NSString. Sets the RhoConnect server url, for example: “http://<ip>:<port>/application
”
@property(assign, setter=setSyncServer) NSString* sync_server;
Initializes your client database. Call this method before you create and use a client.
:+ (void) initDatabase;
Adds your model objects to your client database, allowing them to sync with the RhoConnect server.
- (void) addModels:(NSArray*)models;
models |
NSArray containing the model objects. |
Resets all data for all models in your local database, and then performs logout.
- (void) database_full_reset_and_logout;
Resets all data for all models in your local database and delete client ID. Use this method for ‘unknown client’ error
- (void) database_client_reset;
Returns a RhoConnectNotify object containing login data after logging your client database into the RhoConnect server.
- (RhoConnectNotify*) loginWithUser: (NSString*) user pwd:(NSString*) pwd;
usr |
NSString containing the user name. |
pwd |
NSString containing the password. |
Logs your client database into the RhoConnect server and calls a selector callback target method when the login process is finished.
- (void) loginWithUser: (NSString*) user pwd:(NSString*) pwd callback:(SEL) callback target:(id)target;
usr |
NSString containing the user name. |
pwd |
NSString containing the password. |
callback:(SEL) callback target:(id)target |
The reference to the callback method. An example could be callback:@selector(loginComplete:) target:self . You would write the callback method loginComplete to perform actions when the login process completes. |
Set the sync notification callback method.
- (void) setNotification: (SEL) callback target:(id)target;
callback:(SEL) target:(id)target |
The reference to the callback method that performs actions upon a sync notification. |
Instance method. Clear the sync notification callback.
- (void) clearNotification;
Instance method. Returns true if your RhoConnect session is logged in (if the login session exists in the database), false otherwise.
- (BOOL) is_logged_in;
Instance method. Returns a RhoConnectNotify object after running a sync on all the models for your RhoConnect client.
- (RhoConnectNotify*) syncAll;
Instance method. Returns TRUE if synchronization is in progress
- (BOOL) is_syncing;
Instance method. Stop current sync if running
- (void) stop_sync;
Instance method. Returns a RhoConnectNotify object after sending a search request to the RhoConnect server.
- (RhoConnectNotify*) search: (NSArray*)models from: (NSString*) from params: (NSString*)params sync_changes: (BOOL)
sync_changes progress_step: (int) progress_step;
models |
NSArray containing the names of the model names to search in. |
from |
(Optional) NSString. Sets the RhoConnect path that the records are fetched with. Default = "search". |
params |
NSString. A string containing the name/value search items, in the form "param1=val1¶m2=val2¶m3=val3". The values must be url-encoded. |
sync_changes |
BOOL. TRUE if data with local changes is pushed to the server before the search is performed, FALSE otherwise. |
progress_step |
(Optional) int. Define how often the search callback will be executed when the RhoConnectNotify status is "in_progress". |
Instance method. Sets the callback method for object notification. The callback receives a RhoConnectObjectNotify object as a parameter. This RhoConnectObjectNotify object contains three arrays of hashes for the objects and their source ids that have been updated, created, and deleted, allowing you to display which records were changed.
- (void) setObjectNotification: (SEL) callback target:(id)target;
(SEL) callback target:(id)target |
The name of your callback method. |
- (void) clearObjectNotification;
Instance method. Clears the callback method for object notification.
Instance method. Add an object to track changes: create, update, delete.
- (void) addObjectNotify: (int) nSrcID szObject:(NSString*) szObject;
nSrcID |
int. The object ID of the source on the server with which this RHoConnectClient syncs. You can provide this with the "source\_id" property value. |
szObject |
NSString. The object ID of the item created to track the changes to this RhoConnectClient. |
Sample call:
RhoConnectClient* sclient; ... [sclient addObjectNotify: [[item objectForKey:@"source_id"] intValue] szObject:[item valueForKey:@"object"] ];
Instance method. To process ‘create-error’ errors from server.
- (void) onCreateError: (RhoConnectNotify*)notify action: (NSString*)action;
notify |
RhoConnectNotify. Notification object passed to notification callback. |
action |
NSString. May be "delete" or "recreate": "delete" just remove object from client, "recreate" will push this object to server again at next sync. |
Sample call:
RhoConnectClient* sclient; ... if ([notify hasCreateErrors]) [sclient onCreateError: notify action:@"delete" ];
Instance method. To process ‘update-error’ errors from server.
- (void) onUpdateError: (RhoConnectNotify*)notify action: (NSString*)action;
notify |
RhoConnectNotify. Notification object passed to notification callback. |
action |
NSString. May be "retry" or "rollback": "retry" will push update object operation to server again at next sync, "rollback" will write rollback objects(comes from server) to client database. |
Sample call:
RhoConnectClient* sclient; ... if ([notify hasUpdateErrors]) [sclient onUpdateError: notify action:@"retry" ];
Instance method. To process ‘delete-error’ errors from server.
- (void) onDeleteError: (RhoConnectNotify*)notify action: (NSString*)action;
notify |
RhoConnectNotify. Notification object passed to notification callback. |
action |
NSString. May be "retry" - will push delete object operation to server again at next sync. |
Sample call:
RhoConnectClient* sclient; ... if ([notify hasDeleteErrors]) [sclient onDeleteError: notify action:@"retry" ];
The RhomModel class contains the following properties and methods for setting and using RhomModel objects; RhomModel objects are RhoConnect models and their attributes.
NSString. Sets the model name.
@property(assign) NSString* name;
int. Sets the synchronization type: RST_INCREMENTAL or RST_BULK_ONLY.
@property(assign) int sync_type;
int. Sets the model type: RMT_PROPERTY_BAG (default) or RMT_PROPERTY_FIXEDSCHEMA.
@property(assign) int model_type;
The associations dictionary is a property of the model that controls its synchronization process. When one model (model A) has an association with another model (model B), this forces the other model (model B) synchronized along with the model (model A).
// Associations dictionary: the key is the model attribute name, // the value is the associated model name. // While using associations, use the object_id attribute as object reference @property(assign) NSDictionary* associations;
Example of associating two models, customer and product:
RhomModel* order = [[RhomModel alloc] init]; order.name = @"Order"; order.associations = [NSDictionary dictionaryWithObjectsAndKeys: @"Customer", @"customer", @"Product", @"product", nil];
Initializes a model object.
- (id) init;
Returns a RhoConnectNotify object containing sync information, after running a sync on this model.
- (RhoConnectNotify*) sync;
Runs a sync on this model, and calls a callback, passing the callback the RhoConnectNotify status parameter for the synchronization: “in_progress”, “ok”, “error”, “complete”.
- (void) sync: (SEL) callback target:(id)target;
Example:
int shouldSyncProductByName() { RhoConnectNotify* res = [[product sync] retain]; int nErr = res.error_code; [res release]; if ( nErr!= RHO_ERR_NONE ) { return 0; } return 1; }
Set the sync notification callback method for the Model.
- (void) setNotification: (SEL) callback target:(id)target;
callback:(SEL) target:(id)target |
The reference to the callback method that performs actions upon a sync notification. |
Instance method. Clear the sync notification callback for the Model.
- (void) clearNotification;
Create a model object with attributes and save it to the database, the object id will be generated automatically if not set.
- (void) create: (NSMutableDictionary *) data;
data |
NSMutableDictionary. The object containing the attribute data for this model. |
Returns an NSMutableDictionary object containing a model with the given object id.
- (NSMutableDictionary *) find: (NSString*)object_id;
object_id |
NSString. The object id. |
Returns an NSMutableDictionary object containing the first model object found with the given set of model attributes.
- (NSMutableDictionary *) find_first: (NSDictionary *)cond;
cond |
A NSDictionary object containing the model attributes to find. |
Example call:
NSMutableDictionary* cond = [[NSMutableDictionary alloc] init]; [cond setValue: @"SomeRealName" forKey:@"name"]; prod = [[product find_first: cond] retain]; [cond release]; if ( !prod ) { // not found }
Returns a NSMutableArray containing all the model objects that match the given set of attributes.
- (NSMutableArray *) find_all: (NSDictionary *)cond;
cond |
A NSDictionary object containing the model attributes to find. |
Saves a model object with the given data to the database.
- (void) save: (NSDictionary *)data;
data |
A NSDictionary object containing the attribute data for the model object that is saved. |
Delete model objects from the database that contain the given data.
- (void) destroy: (NSDictionary *)data;
data |
A NSDictionary object containing the attribute data for the model objects that are to be deleted. |
Run this method when you start to create or update a bunch of models; it will optimize database access. Equal to start transaction.
- (void) startBulkUpdate;
Run this method when you start to create or update a bunch of models; it will optimize database access. Equal to commit transaction.
- (void) stopBulkUpdate;
Check does model contain non-synced local changes
- (BOOL) is_changed;
Several RhoConnectClient methods return a RhoConnectNotify object. The properties for the RhoConnectNotify class provide access to sync notification objects as described here. (Developers do not set RhoConnectNotify properties, but only read them after a call to a RhoConnectClient method that returns a RhoConnectNotify object.)
total_count |
@property int. Total number of records that exist for this RhoConnect source. |
processed_count |
@property int. Number of records included in this sync page for an in\_progress (status = incremental) sync. |
cumulative_count |
@property int. Number of records the SyncEngine has processed so far for this source. |
source_id |
@property int. The id of the current model that is synchronizing. |
error_code |
@property int. The HTTP response code of the RhoConnect server error: 401, 500, 404, etc. |
source_name |
@property(assign) NSString*. Name of the model (i.e. "Product"). |
status |
@property(assign) NSString*. Status of the current sync process: "in\_progress", "error", "ok", "complete", "schema-changed". |
sync_type |
@property(assign) NSString*. Type of sync used for this model: "incremental" or "bulk". |
error_message |
@property(assign) NSString*. Response body of error message (if any). |
callback_params |
@property(assign) NSString*. Callback parameters. |
hasCreateErrors |
(Boolean). Return true if server return create-errors. |
hasUpdateErrors |
(Boolean). Return true if server return update-errors. |
hasDeleteErrors |
(Boolean). Return true if server return delete-errors. |
isUnknownClientError |
(Boolean). Return true if server return unknown client error. |
This class provides access to the sync object notification as described here.
The RhoConnectObjectNotify object contains three arrays of hashes for the objects and their source ids that have been updated, created, and deleted. Each hash contains values for the keys “object” and “source_id” so you can display which records were changed.
deleted_objects |
@property(readonly) NSMutableArray*. An array containing the deleted objects. |
updated_objects |
@property(readonly) NSMutableArray*. An array containing the updated objects. |
created_objects |
@property(readonly) NSMutableArray*. An array containing the created objects. |
deleted_source_ids |
@property(readonly) NSMutableArray*. An array containing the deleted source ids. |
updated_source_ids |
@property(readonly) NSMutableArray*. An array containing the updated source ids. |
created_source_ids |
@property(readonly) NSMutableArray*. An array containing the created source ids. |
To package the RhoConnect Client archive for distribution, go to the top of the rhodes repository and run:
$ rake build:rhoconnect_client
This will produce a zipfile in the folder called rhoconnect-client-<someversion>.zip
where <someversion>
is the version of the client.
Unzip package to some folder
Open project rhoconnect-client\ObjectiveC\Tests\RhoConnectClientTest
in xcode and run. See log - SUCCESS should be at the end of log.
Open project rhoconnect-client\Samples\ObjectiveC\store
in xcode and run. Press Login, you should see several items, click on item, you should see details.