The Client Project Generated by the Data Abstract New Project Wizard
The aim of this article is provide a deeper look at the project you previously created using the Data Abstract New Project Wizard. It will also show you what you need to do to complete your client application so that it talks to either an instance of Relativity Server or a custom Data Abstract server.
Files in the Project
General overview of the basic files
In depth with DataAccess.h & DataAccess.m
Data Abstract for Cocoa uses the typical Cocoa framework convention of starting the first two letters of the classname to avoid class overlaps. Therefore throughout the Data Abstract classes you will find that they either start with DA (for Data Abstract) and RO which is the prefix used for classes from the Remoting SDK which is the underlying remoting framework that Data Abstract uses to communicate with the server.
With that in mind, the most important files in the project created by the project wizard is the DataAccess class which can be found in DataAccess.h and DataAccess.m. The singleton class, available anywhere via the
sharedInstance property, acts as the bridge between your application and the server you have chosen to use. It is responsible for:
- The connection and login to Relativity Server or a custom Data Abstract server
- Downloading, persisting and exposing data as needed; for instance to
- Tracking changes to the data and applying them back to the server.
The class as it stands is incomplete, you will be working with it to make it suitable for the schema, tables and fields that are specific to your application.
Class implements two delegates
DARemoteDataAdapterDelegatedelegate is available in
DARemoteDataAdapter.h. All the methods in the delegate are optional, however you should implement one of the
verifiedPasswordmethods as appropriate. These are used by the remote data adapter to authenticate you with the server.
@protocol DARemoteDataAdapterDelegate <NSObject> @optional - (void)remoteDataAdapter:(DARemoteDataAdapter * )adapter didFailToApplyChange:(DADeltaChange * )change forTable:(DADataTable * )table; - (void)remoteDataAdapter:(DARemoteDataAdapter * )adapter didFailWithException:(NSException * )exception forAsyncRequest:(DAAsyncRequest * )request; - (BOOL)remoteDataAdapterNeedsLogin:(DARemoteDataAdapter * )adapter __deprecated; - (BOOL)remoteDataAdapterNeedsLoginOnMainThread:(DARemoteDataAdapter * )adapter __deprecated; - (BOOL)remoteDataAdapter:(DARemoteDataAdapter * )adapter needsPassword:(NSString ** )password forLoginString:(NSString * )loginString; - (BOOL)remoteDataAdapter:(DARemoteDataAdapter * )adapter needsPassword:(NSString ** )password forUsername:(NSString * )username; - (BOOL)remoteDataAdapter:(DARemoteDataAdapter * )adapter verifiedPassword:(NSString * )password forLoginString:(NSString * )loginString; - (BOOL)remoteDataAdapter:(DARemoteDataAdapter * )adapter verifiedPassword:(NSString * )password forUsername:(NSString * )username; @end
DAAsyncRequestDelegatedelegate is available in
DAAsyncRequest.h, again all the methods in the delegate are optional however if you want to receive updated data from the server you will need to implement one of the
@protocol DAAsyncRequestDelegate @optional - (void)asyncRequest:(DAAsyncRequest * )request didReceiveSchema:(DASchema * )schema; - (void)asyncRequest:(DAAsyncRequest * )request didReceiveTable:(DADataTable * )table; - (void)asyncRequest:(DAAsyncRequest * )request didReceiveTables:(NSDictionary * )tables; - (void)asyncRequest:(DAAsyncRequest * )request didReceiveData:(NSData * )data; - (void)asyncRequest:(DAAsyncRequest * )request didFailToApplyChange:(DADeltaChange * )change forTable:(DADataTable * )table; - (void)asyncRequest:(DAAsyncRequest * )request didFinishApplyingChangesForTables:(NSArray * )tables withErrors:(NSArray * )errors; - (void)asyncRequest:(ROAsyncRequest * )request didFailWithException:(NSException * )exception; - (void)asyncRequest:(ROAsyncRequest * )request didReceiveDataSize:(int) size ofExpected:(int) totalSize; - (void)asyncRequest:(DAAsyncRequest * )request didFinishExecutingCommand:(NSString * )commandName withResult:(int)result andOutputParams:(NSDictionary * )outParams; @end
DataAccess class has 4 public properties available to your classes. They are:
@property (assign) id<DataAccessDelegate> delegate; @property (readonly) NSString *homeFolder; @property (readonly) BOOL dataReady; @property (readonly) BOOL busy;
delegateproperty can be used to store a reference to a class you create that implements the
homeFolderspecifies the folder which the
DataAccessclass will use to store data. It is generated in the
initmethod using the value specified in
dataReadyindicates that the data is available. It is set by the method
triggerDataReadywhich also posts the
NOTIFICATION_DATA_READYnotification which you can listen for to retrieve the data when its available.
busyIs used to indicate if the class is busy transacting with the server. The set method is overridden to post a
NOTIFICATION_BUSYSTATUS_CHANGEnotification whenever the state changes. Your applications can listen for this notification so that you can queue up operations.
There are also four public methods, plus a class method that returns a singleton instance of the class:
+ (DataAccess *)sharedInstance; - (void)loadInitialData; - (void)saveData; - (void)saveDataInBackground; - (DAAsyncRequest *)beginApplyUpdates;
sharedInstanceis a class method that returns a singleton instance of the class, creating it if needed.
loadInitialDatamethod is used to trigger an initial loading of the data in a background thread. Initially
DataAccesswill attempt to load data that was previously saved to a briefcase and if that isn't available, it will attempt to download the data from the server.
WARNING: Never call
loadInitialData asynchronously, as it itself calls
threadedLoadInitialData in a background thread. This could cause problems with passing results back into the UI thread.
saveDataInBackgroundmethods are both used when your application is using a briefcase to store the data offline.
beginApplyUpdatesmethod is used asynchronously apply local data changes back to the server. You will need to edit this method to add the tables whose changes should be sent to the server.
DataAccess class header provides a delegate protocol, that a class wanting to be a delegate needs to implement.
@protocol DataAccessDelegate - (void)alertError:(NSString * )message; - (BOOL)needLogin:(NSString ** )login password:(NSString ** )password; @end
DataAccess class uses the delegate to inform you when it requires login details, or to let you know something has gone wrong. The username and password are passed from your implementation of
needLogin:password: to the caller.
The actual implementation of
DataAccess that we provide is a combination of fully functional code and partial method stubs that you will need to implement. Generally the method stubs contain commented example code showing you how a typical method implementation might look like. As noted above, the class adopts the
DAAsyncRequestDelegate delegate protocols, and implements some, but not all, of the optional delegate methods.
While we provide
DataAccess, the expectation is that you will adapted the class to your needs. Implementing the delegate methods of
DAAsyncRequestDelegate as appropriate for your use, and adding methods to retrieve data or schemas as needed.
Below we will step through the finer details of the class.
At the beginning of
DataAccess.m you will find a large section of
#pragma mark - #pragma mark Defines #define APPLICATION_SUPPORT_FOLDER @"RemObjects/DAMacApp" #define BRIEFCASE_FILENAME @"DAMacApp.briefcase" #define BRIEFCASE_DATA_VERSION_KEY @"DAMacApp-BriefcaseVersion" #define BRIEFCASE_DATA_VERSION @"0.1" // The following defines specify the name under which passwords // will be saved in the keychain #define KEYCHAIN_APPNAME @"RemObjects.DAMacApp" // The following defines specify the KEYS under which username, // password and server address will be stored in NSUserDefaults: #define USERDEFAULTS_SERVERADDRESS_KEY @"ServerAddress" #define USERDEFAULTS_USERNAME_KEY @"UserName" #define USERDEFAULTS_PASSWORD_KEY @"Password" // Uncomment the following define to use AES encryption. // the password should be the same as on server side. //#define AES_PASSWORD @"type AES password here" #warning Define your server address and Relativity domain name, below. #define SERVER_URL @"http://localhost:7099/bin" // Do not change, when using Relativity Server #define SERVICE_NAME @"DataService" #define RELATIVITY_DOMAIN @"MyDomain" //#define RELATIVITY_SCHEMA @"MySchema"
A number of them are generated based on your project name (like the
APPLICATION_SUPPORT_FOLDER) and don't require you to make any changes to them. However there are a key few which require your input:
BRIEFCASE_DATA_VERSION- is your way of indicating that the data stored in a briefcase is out of data and shouldn't be loaded. If you release a new version of the client that needs to use a updated version of the dataset from the server, then bump the
BRIEFCASE_DATA_VERSIONnumber and when the new client attempts to load the data in
threadedLoadInitialDatait will see the version mismatch and instead retrieve the data from the server.
AES_PASSWORDthis comes commented out. If your server is using AES encryption then you should uncomment it and set the password to match that on the server.
SERVER_URL- should be changed to the address of your server (be it an instance of Relativity which is normally on port 7099, or an instance of a custom Data Abstract server). It is set to localhost by default as the assumption is you are developing against a local copy of the data.
RELATIVITY_SCHEMAshould be changed to match the names of the and schema you are using.
You may have noticed the #warning in amongst the #defines. We have used #warning throughout a number of files in the generated project to indicate where you need to make changes to the code. We'll cover more on that in the section below.
initsets up some basic things about the class. The first thing it does is to create a folder that will be used to store briefcase files, and sets up the
briefcaseFileNamewhich will be used when you go to save the data locally. It then sets up a RemoteDataAdapter and sets the
DataAccessas a delegate for it. If you need to do any additional initilization, then add it to the end of the method.
loadInitialDatais called from
AppDelegate.mto begin the process of loading the data.
threadedLoadInitialDatain a background thread and then returns control to the
threadedLoadInitialDataas noted above, this method operates in a background thread to load the data. Initially the method attempts to load the data from a briefcase if a briefcase file exists. If it doesn't OR if there is a version mismatch (
BRIEFCASE_DATA_VERSION_KEY) between the client and the briefcase data, then the data is retrieved in a synchronous fashion from the server. Once the data is available,
setupDatais called to perform any additional setup and then
triggerDataReadyis called on the main thread and posts the
NOTIFICATION_DATA_READYnotification. Note that it is intended that this method is never called directly.
downloadDatais called by
threadedLoadInitialDatato download data from the remote server. You are required to provide the body of this method otherwise it will do nothing. At its simplest you need to call the
getDataTablemethod of DARemoteDataAdapter, passing in the name of the table you are interested in. Alternatively you could pass in an array of table names to DARemoteDataAdapter's
getDataTables. When you have implemented the method, you can delete the
#warningline to remove the build warning.
Note that the
getDataTable* methods of DARemoteDataAdapter are synchronous methods, meaning that they block the code and will directly return the data. DARemoteDataAdapter also provides asynchronous methods (those starting with
async*). For an initial download of the data that is required for your application to work, then the synchronous methods will likely be the most suitable.
The totally optional
saveDataToBriefcase:methods work in tandem to load and save your data tables to disk, to persist the data between runs of your application. If you don't need to persist the data, you don't need to worry about implementing the methods. Both methods have their method bodies commented out, but provide example code showing you how to use the briefcase. To save data, simply add your table to the briefcase using the
addTable:method of DABriefcase and supply a
BRIEFCASE_DATA_VERSIONwhich is checked before the data is loaded; if you don't supply a
BRIEFCASE_DATA_VERSIONthen DataAccess will not load the briefcase data. Retrieving data from the briefcase is as simple as calling the
tableNamedmethod of DABriefcase.
setupDatais an internal method called by
threadedLoadInitialDataafter the data has either been loaded from a briefcase or downloaded from the server. The intention is that you would use the method to set up any virtual fields (for instance lookup or calculated fields) in your instance of DADataTable. The body of the method is unimplemented, but provides a commented out example of how you might use it to add a calculated field.
YESand posts a
NOTIFICATION_DATA_READYnotification which you can listen to in your app; for instance you can use it to know to update the UI.
triggerDataReadyis called by
threadedLoadInitialDataafter the data has been loaded (either from a briefcase or directly from the server) and is available for use.
saveDataInBackgroundcalls fires of
saveDatain a background thread. In the project created it is only called by
asyncRequest:didFinishApplyingChangesForTables:withErrorsto save a local copy of the data to a briefcase. You might also call this method from your own app when the user goes to quit your app.
The setter method (
setBusy:) of the
busyproperty is overridden to post a
NOTIFICATION_BUSYSTATUS_CHANGEnotification when the value is changed. A number of methods check the state of
busy, dropping out if
DataAccessis busy. You can listen for the notification so that you can attempt to call whichever operation you were attempting.
remoteDataAdapterNeedsLogin:is a delegate method of
DARemoteDataAdapterDelegatedeclared in the header of DARemoteDataAdapter. The delegate method is called anytime the remote data adapter DARemoteDataAdapter needs to authenticate the caller. This is one of the longest methods in
DataAccessas it handles both the Mac and iOS platforms. It starts by checking if the class that implements the
DataAccessDelegateprotocol has implemented the
needLogin:password:method, if it doesn't then it returns
NO. Otherwise it first attempts to get the login username and password from NSUserDefaults, and then generates a login string which is passed to the
loginWithStringmethod of DARemoteDataAdapter. If that fails (or there was no username/password in NSUserDefaults) then the username and password are retrieved from the
needLogin:password:, a new login string is created and passed to
loginWithString. If this fails 3 times, then the login attempt is declared a failure.
beginApplyUpdatesattempts to apply any local changes to the data to the remote server in an asynchronous way. For it to work properly, you first need to specify which tables require their changes to be saved, you do that by adding them to the
a, which will then be passed to the
beginApplyChangesForTablesof DARemoteDataAdapter. The
DataAccessclass is then added as a delegate of the
DAAsyncRequestclass (as noted above, the
DataAccessclass implements the
DAAsyncRequestDelegateprotocol), this means that when remote operation is complete, the appropriate
asyncRequest*method is called on
DataAccess. The context property allows you to specify an ID for the request, that you can then later check for when the delegate method is called with the results of the
The intention is that you would call this method from your main application at a point in time that is most applicable to you or the user (for instance, it might be called explicitly by the user using an "Apply Updates" button).
asyncRequest:didReceiveTablesdelegate methods are called after the successful completion of a
DAAsyncRequestrequest, one of the arguments is the new version of the DADataTable. While both methods appear in
DataAccess, neither provides a default implementation. You need to do something with the table, be it call a delegate on another class, post a notification that another class is listening for, or update some internal data store. If multiple requests are possible, then you can filter based on the context property of
There are then three delegate methods for handling error conditions. The default implementations output the error message to
NSLogand then passes the error message to the
alertError:delegate method. It is expected that you would modify these methods to be more appropriate for your application:
asyncRequest:didFinishApplyingChangesForTables:withErrorsare basically the same, they handle the case that an attempt to apply a delta change to the server data fails. If you attempt to update multiple tables at the same time but only implement the
asyncRequest:didFailToApplyChange:forTable:delegate, then it will be called once per table.
asyncRequest:didFailWithExceptionis intended to handle more general exceptions like the network connection failing.
The last section of the
DataAccess class that you need to work with is
#pragma mark Getting More Data section. This section is intended for you to add data request methods that will fetch additional data on demand, for instance if you want to refresh the data you received on start up (maybe the user is working with a shared shopping list) to pick up any recent remote changes. The commented out code gives an example of creating both an synchronous and asynchronous data request.
A Closer look at the warnings
As noted in the article about creating a project, if you try and build the newly created project you will get a bunch of build warnings, like those in the figures below.
These warnings aren't because we shipped a broken template; instead they are used to indicate points in the code where you need to make changes to make the project work with your server. Once you've made the required changes, you can safely delete the
The warning in
needLogin:password: is to get you to properly implement requesting the username and password information from the user. The intention is that this will only be called when no login information is stored in Settings/the Keychain. By default it returns the stock username and password which are used for a new domain in Relativity before an alternative login provider is specified.
There is then a warning in the #defines of
DataAccess.m where you need to change the
SERVICE_URL to point to your server, and then set the
RELATIVITY_SCHEMA to match those you are using in your server.
There is a warning in
DataAccess. As noted above you need to implement this class so that it downloads the initial data you need for your application.
Then there is a warning in both
DataAccess, the intention is that you specify the tables to be saved, and then correspondingly specify those that should be loaded.
In the iOS version of the project there are 3 additional warnings, which can be found in
The first warning is in the method
myTable which should return an instance of the DADataTable you are using. You would in theory rename it to something more suitable for your application.
Next there is a warning in
tableView:cellForRowAtIndexPath: where you need to implement how the data should be displayed in the table's cells.
Finally you will find a warning in
tableView:didSelectRowAtIndexPath:, here you should implement the method to react to cells that have been touched.