Dynamic Reporting / Computing in IOS

Jacob Van Brunt • Jun 05, 2017

Recently I had a client who needed to be able to change reporting and data algorithms on the fly, without having to push a new iOS build each time. Compliance standards required that all data be stored on the device, and never sent to the cloud, so everything had to be computed on the device itself. I needed to create a solution that would meet the client’s needs while also allowing me, or the next developer, to easily update the logic.

I could easily write a utility class inside the application with a list of stored functions, but that requires a new build to be published with every change request. With reporting and data analysis that have the potential to change every day, that would be frustrating for both the client and myself. The utility class route would also set me up for failure later if I had to migrate to Android. Making the tedious grind of reimplementing it in Java. (I mostly work in native applications)


JAVASCRIPT TO THE RESCUE.

Android and iOS can both process Javascript files and return its output to the native application. This gave me the idea to create small Javascript files that contain small stored procedures. Each file contains one entry method, which can take any number of parameters specified. I called this method execute.

function execute(start, end, callback) {

var context = Context.context();

var entries = context.queryEntriesBetweenDateStartEnd(start, end);

callback(entries.length > 0);

}

This simple method finds database entries between the designated parameters. Super simple, but there are few things needed to make this work efficiently.

function execute(start, end, callback)

My function declaration states there are two passed in parameters and a callback. Why is there a callback? Some of you may already know, but for those who don’t, if a database has a lot of entries, it could take a few seconds to execute and return. Common practice would be to execute this method asynchronously on a background thread, so we don’t block the main thread. With the help of iOS’s JavascriptCore library and ReactiveCocoa, I created a nice execution service object.

- (RACSignal *)evaluateAsync:(NSURL *)url args:(NSArray *)args {

    __block NSArray *blockArgs = [args copy];

    @weakify(self);

    return [[RACSignal createSignal:^RACDisposable *(id subscriber) {

        @strongify(self);

        if (!blockArgs) {
            blockArgs = @[];
        }

        void(^callback)(JSValue *) = ^(JSValue *value){
            [subscriber sendNext:value];
            [subscriber sendCompleted];
        };

        [self evaluate:url args:[blockArgs arrayByAddingObject:callback]];
        return nil;

    }] subscribeOn:[RACScheduler schedulerWithPriority:RACSchedulerPriorityBackground]];
}

static NSString *const kExecutionMethod = @"execute";

- (id)evaluate:(NSURL *)url args:(NSArray *)args {

    NSString *script = [self.fileManager loadJS:url];

    [self.context evaluateScript:script withSourceURL:url];

    JSValue *execute = self.context[kExecutionMethod];

    JSValue *result = [execute callWithArguments:args];

    return result;

}

- (void(^)(JSContext *context, JSValue *value)) exceptionHandler{

    return ^(JSContext *context, JSValue *exception) {

        NSString *stack = exception[@"stack"].toString;

        NSString *line = exception[@"line"].toString;

        NSLog(@"JS Error: %@ in method %@ line number %@", exception, stack, line);

    };
}

Now that I have a way of executing Javascript, I needed a way for my Javascript files to communicate with the local database. That is where the context object comes into play. My context object is an object that adheres to the JavascriptCore JSExport protocol.

@protocol Context

+ (instancetype)context;

- (NSArray *)queryEntriesBetweenDateStart:(NSString *)start end:(NSString *)end;

It’s basically a helper that has a list of generic methods for accessing data in the database. ie querying, saving, deleting, etc. You can make this more powerful by creating a query language, but that is for another day.

The context object is injected into every Javascript file, which allows for testability in the the file itself, as well in the native application.

Note: Any object referenced in the Javascript file must adhere to the JSExport library, this allows you to access variables and methods. You can learn more by reading the JavascriptCore documentation.

Now that we have a way of executing these files they can be combined to create larger more complex logic. The next question is how do we update these files on a daily basis? That is actually the easy part.

Most mobile applications communicate to some sort of REST API, whether to authenticate, get application data, or to push data. So I created a REST API, that serves up these files for the application to download.

var express = require('express');
var router = express.Router();
var fs = require('fs');

router.get('/:name', function(req, res, next) {

var params = req.params;
var name = params.name;

var options = {
root: './resources/',
dotfiles: 'allow',
headers: {
'xtimestamp': Date.now(),
'xsent': true
}
};

res.sendFile(name, options, function(error) {

if (error) {
    res.sendStatus(error.status).end();
}
});
});
module.exports = router;

On startup my device downloads the newest version of the file. You could add some logic to check when the file was last updated and only download after the last sync date, but for demo purposes I just download them every time.

- (RACSignal *)download:(NSURL *)url {

if (!url) {

return [RACSignal return:nil];

}

@weakify(self);

return [[RACSignal createSignal:^RACDisposable *(id subscriber) {

@strongify(self);

NSURL *url = [[self.baseURL URLByAppendingPathComponent:@"uploads"] URLByAppendingPathComponent:url.lastPathComponent];

NSURLRequest *request = [NSURLRequest requestWithURL:url];

NSURLSessionDownloadTask *task = [self.sessionManager downloadTaskWithRequest:request progress:nil destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {

NSURL *documentsURL = [self.fileManager URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:false error:nil];

NSURL *fileURL = [documentsURL URLByAppendingPathComponent:response.suggestedFilename];

NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;

if (httpResponse.statusCode == 200) {

[self.fileManager removeItemAtURL:fileURL error:nil];

}

return fileURL;

} completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {

if (error) {

[subscriber sendError:error];

} else {

[subscriber sendNext:filePath];

[subscriber sendCompleted];

}

}];

[task resume];

return [RACDisposable disposableWithBlock:^{

[task cancel];

}];

}] subscribeOn:[RACScheduler schedulerWithPriority:RACSchedulerPriorityBackground]];

}

Now that my demo product has a REST API and can execute Javascript files on the fly, creating new or updating computing/reporting logic is as simple as updating or uploading a new JS file to the REST API.

As a mobile developer I loathe having to make new builds, dealing with provisioning profiles, etc every time a simple logic change is required. This approach allows for me to update logic easily or others to create CMS to do the same, without having to make a new release.