How to call two React Native native module methods at the same time? - objective-c

Ok, this is a bit complicated. I can't link all of my code because it's huge, but I'll do my best to give some representative pseduo code in it's place.
I have an ios React Native project and in xcode I have embdeded a custom objective C framework in my project. To access that objective C framework I've created a Native Module that acts as a wrapper/bridge.
So far this has all worked great - essentially my user clicks on a button, which triggers the objective c framework via a native module, the objective c framework takes about 30 seconds to a minute to finish processing the user's selected data and then spits out the uri to a processed file.
The problem I've run into is that my users are confused as to whether or not my app is frozen. As it happens the main function of my framework uses a for loop, so I decided to add two properies to the framework's main class and add a function that returns the properties so we can see the progress. It looks like this:
#implementation MyFramework {
int currentBlock;
int allBlocks;
}
- (NSArray<NSString*>*)processFile:(NSString *)file
{
int totalBlocks = examineFile(file);
for( int iBlock= 0; iBlock<totalBlocks; iBlock++) {
currentBlock = iBlock;
//other code that actually does stuff to the file goes here
}
//code that stitches the blocks together and generates files goes here
NSArray* files = #[filePath1, filePath2, filepath3];
return files;
}
- (NSArray<NSString*>*)getProgress:(NSString *)msg
{
int* pCurrentBlock = &currentBlock;
int* pAllBlocks = &allBlocks;
NSString* currentBlockString = [NSString stringWithFormat:#"%d", *pCurrentBlock];
NSString* allBlocksString = [NSString stringWithFormat:#"%d", *pAllBlocks];
NSArray* progressArray = #[currentBlockString, allBlocksString];
return progressArray;
}
Back in Javascript I call a Native Module wrapper for the above code. So when the user clicks a button I trigger a Native Module method called getFiles() which in turn calls the processFile() method in Objective C code above.
Now, while the processFile() method is running I simultaneously kick off a setInterval() every 2 seconds that calls a second Native Module method called status() which calls the getProgress() function shown above. The big problem I have is that getProgress() will not return a result while processFile() is running. Instead it waits until processFile() method has finished and then it returns around 20 results all at once.
So instead of getting updates like this:
['1','37']
['2','37']
['3','37']
['4','37']
['5','37']
I get something like this all at the end
['36','37']
['36','37']
['36','37']
['36','37']
['36','37']
Here's my Native Module code
const MyFramework *myLocalFramework = [[MyFramework alloc]init];
RCT_EXPORT_MODULE();
RCT_EXPORT_METHOD(getFiles:(NSString *)path
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSString *modelFilePath = [[NSBundle mainBundle] pathForResource:#"converted_model_float16" ofType:#"tflite"];
NSArray * files = [myLocalFramework localFile:path tfModel:modelFilePath];
if (files) {
resolve(files);
} else {
reject(#"event_failure", #"no event id returned", nil);
}
}
RCT_EXPORT_METHOD(status:(NSString *)path
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSString* hi = #"hi";
NSArray* percentages = [myLocalFramework getProgress:hi];
if (percentages) {
resolve(percentages);
} else {
reject(#"event_failure", #"no event id returned", nil);
}
}
So with all that said, if anyone has any ideas for how I can get the Native Module to run the status() method without waiting for getFiles() to finish first, I would love your input!

Related

Swift + Objective C - Extra Argument 'withCompletionBlock' in call

I'm a little new to Swift and Objective-C (and by that I mean I started last week) so if this is a duplicate question I'm sorry in advance.
In short I seem to be having trouble with calling a method. One part of the code is in Objective-C and the other part is in Swift. I imported the Objective-C file in to the Swift file through a bridging header file if that helps.
Declaration of method (in Objective-C):
- (NSUInteger)fetchTeamsForEventKey:(NSString *)eventKey withCompletionBlock:(void (^)(NSArray *teams, NSInteger totalCount, NSError *error))completionBlock {
NSString *apiMethod = [NSString stringWithFormat:#"event/%#/teams", eventKey];
NSUInteger taskId = [[TBAKit sharedKit] callArrayMethod:apiMethod modelClass:[TBATeam class] andCompletionBlock:^(NSArray *modelObjects, NSInteger totalCount, NSError *error) {
completionBlock(modelObjects, totalCount, error);
}];
return taskId;
}
Implementation of method (Swift):
TBAKit.fetchTeamsForEventKey("2015cacg", withCompletionBlock: {(teams:NSArray, totalCount:NSInteger, error:NSError) in //The error appears in this line.
if error {
NSLog("Unable to fetch event - %#", error.localizedDescription)
return
}
teamList = teams
})
The source code for the declaration method is in this GitHub Repo: https://github.com/ZachOrr/TBAKit. It's in the file TBAKit/TBAKit+EventMethods.m
Edit: I also tried the code with each of the following:
-> UInt
and
-> Int
Edit 2: So the solution mentioned by t4nhpt did work as it was intended to, but in turn another problem presented itself. I had to replace
teamList = teams
with
self.teamList = teams
Now I'm getting the error: "cannot assign a value of type 'anyobject' to a value of type '[TBATeam]'" on the line
self.teamList = teams
My declaration of the variable is
var teamList = [TBATeam]()
Edit 3: I solved the problem specified in EDIT 2 by converting each object inside teams to TBATeam. Sorry for the stupid mistake.
Try this:
TBAKit.sharedKit().fetchTeamsForEventKey("2015cacg") { (teams, totalCount, error) -> Void in
// your code
}

How to pass arguments to app built on Phonegap

I'm writing an app using JQM and Phonegap to deploy on iOS and I need it to read input arguments like a url arguments of a common website does in javascript by handling the object 'window.location.search'
In my situation, the app will be launched from a website, like this:
My App
This is working right now, I can already call my app, what I need now is to read the arguments arg1, arg2, etc. I've tried reading window.location.search but with no luck.
How can I do this? Do I need to write some Objective C code?
Any suggestions would be appreciated.
Thanks.
My problem was solved using the content of this link: https://gist.github.com/859540
The code is:
Objective-c part:
In MainViewController.m:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// perform any custom startup stuff you need to ...
// process your launch options
NSArray *keyArray = [launchOptions allKeys];
if ([launchOptions objectForKey:[keyArray objectAtIndex:0]]!=nil)
{
// we store the string, so we can use it later, after the webView loads
NSURL *url = [launchOptions objectForKey:[keyArray objectAtIndex:0]];
self.invokeString = [url absoluteString];
NSLog(#amp;" launchOptions = %#",url); // if you want to see what is happening
}
// call super, because it is super important ( 99% of phonegap functionality starts here )
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
- (void) webViewDidFinishLoad:(UIWebView*) theWebView
{
// only valid if ___PROJECTNAME__-Info.plist specifies a protocol to handle
if (self.invokeString)
{
// this is passed before the deviceready event is fired, so you can access it in js when you receive deviceready
NSString* jsString = [NSString stringWithFormat:#"var invokeString = \"%#\";", self.invokeString];
[theWebView stringByEvaluatingJavaScriptFromString:jsString];
}
// Black base color for background matches the native apps
theWebView.backgroundColor = [UIColor blackColor];
return [super webViewDidFinishLoad:theWebView];
}
In the index.html file using cordova-1.7.0:
function onDeviceReady()
{
alert(invokeString);
}
alert returned: myapp://?arg1=1&arg2=2
just the same string used to call it ... :)
I had the same issue, everything here in these answers is hella confusing and extra information.
Understanding and solving the problem in 2 easy steps:
Informative (you can skip if you don't care what happens in the background): Go to AppDelegate.m in Clases folder in the project and search for "handleOpenUrl", you should notice some code there with comments explaining what's up. I don't know objective-c, but intuitively that code there looks for window.handleOpenURL function and calls it giving it the parameter of the url called (e.g. 'myapp:///?parameter=value')
Basically all you have to do is globally(in window object) define the function handleOpenURL
function handleOpenURL (url) {
alert(url);
}
Note that this only gets executed when your app is opened with an
..
window.location will be the location of your phonegap index.html file, not the URL that was used to launch your app.
Some web searches suggested that a function called:
function handleOpenUrl(url) {
alert("opened from url " + url);
}
.. might automatically be called . I don't have my dev machine here to test though, Sorry!
If this doesn't work in Objective-C check out the handleOpenUrl method of the AppDelegate.m This gets called when your app is opened with a URL Scheme.
you should do it in obj-c and then with a plugin pass it to javascript code :
for doing it in obj-c first you should implement
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSURL *urlToParse = [launchOptions objectForKey:UIApplicationLaunchOptionsURLKey];
if (urlToParse) {
[self application:application handleOpenURL:urlToParse];
}
return YES;
}
and then you could access parameters like this :
- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url {
if ([[url scheme] isEqualToString:#"myapp"]) {
//in here you do whatever you need the app to do
// e.g decode JSON string from base64 to plain text & parse JSON string
}
return YES; //if everything went well
}
function getParameterByName(name)
{
name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
var regexS = "[\\?&]" + name + "=([^&#]*)";
var regex = new RegExp(regexS);
var results = regex.exec(window.location.href);
if(results == null)
return "";
else
return results[1];
}
Call this function like var para1 = getParameterByName("para1");
on pageshow event in jquery mobile.
$('#page').on('pageshow',function(event){
var para1 = getParameterByName("para1");
});

PhoneGap plugin: AudioEncode success callback never called

I'm using the AudioEncode plugin for PhoneGap (Cordova) on iOS. After updating a couple of lines for a newer version of Cordova, it appears to be correctly encoding wav files as m4a. In the Xcode console I see:
AVAssetExportSessionStatusCompleted
doing success callback
When I look at the file system on the phone, the wav file has indeed become a m4a. However, the success callback (where I upload the file to a server) is never run. This is the relevant code in the plugin:
-(void) doSuccessCallback:(NSString*)path {
NSLog(#"doing success callback");
NSString* jsCallback = [NSString stringWithFormat:#"%#(\"%#\");", self.successCallback, path];
[self writeJavascript: jsCallback];
[self.successCallback release];
}
My code in the app goes like this:
function encodeSuccess (path) {
console.log('Audio encoded to M4A! Preparing to upload...')
// file transfer code...
}
console.log('Preparing to encode audio file...')
window.plugins.AudioEncode.encodeAudio(entry.fullPath, encodeSuccess, fail)
I'm assuming the doSuccessCallback function in the plugin needs to be updated, but I don't have experience with Objective C or PhoneGap plugins, so I'm stuck at this point.
Any ideas?
UPDATE
In the Objective C function posted above, I tried logging self.successCallback, and it logged as <null>. Then I went up to the top of the main encodeAudio function, and the argument which is assigned to self.successCallback ([arguments objectAtIndex:1]) also logs as <null>. So, it seems to me that the callbacks are not being passed into the main function successfully.
This is the AudioEncode.h file, maybe someone can spot the problem here:
#interface AudioEncode : CDVPlugin {
NSString* successCallback;
NSString* failCallback;
}
#property (nonatomic, retain) NSString* successCallback;
#property (nonatomic, retain) NSString* failCallback;
- (void)encodeAudio:(NSArray*)arguments withDict:(NSDictionary*)options;
Ok, I figured this out by reading the basic examples in the Cordova Plugin Development Guide closely. The problem was with the ordering of parameters for cordova.exec(), which must have changed recently.
I plan to submit a pull request on GitHub with a working version of the plugin, but for now, here's the basic solution.
Before asking this question, I had updated the imports in AudioEncode.h from #import <PhoneGap/PGPlugin.h> to:
#import <Cordova/CDVPlugin.h>
#import <Cordova/CDVPluginResult.h>
Any reference to PGPlugin should also be updated to CDVPlugin, and PhoneGap should become cordova.
Here's the crux of the problem: in AudioEncode.js, cordova.exec() (where the original plugin calls PhoneGap.exec()) needs to be called like this:
AudioEncode.prototype.encodeAudio = function(audioPath, successCallback, failCallback) {
cordova.exec(successCallback, failCallback, "AudioEncode", "encodeAudio", [audioPath]);
};
If you don't order the parameters like this, the callbacks won't be passed in (although audioPath was...). Look at the docs for more details, but the parameters have to be the two callbacks first, the module name, the module action, and finally an array of extra parameters.
Then, you'll need to read in the parameters in the main encodeAudio function like this:
self.callback = [[arguments objectAtIndex:0] retain];
NSString* audioPath = [arguments objectAtIndex:1];
Note that there is only one callback object now, which contains references to the success and fail callbacks. This means that whenever the plugin sets up variables for successCallback and failCallback, you now only need callback (e.g. #synthesize callback). This is also declared in the AudioEncode.h file with #interface and #property.
Now, when actually firing the callbacks & returning data (in the doSuccessCallback and doFailCallback functions), you need to use CDVPluginResult, like this:
CDVPluginResult* pluginResult = nil;
NSString* javaScript = nil;
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:path];
javaScript = [pluginResult toSuccessCallbackString:self.callback];
[self writeJavascript: javaScript];
[self.callback release];
Until I get the updated module up on GitHub, this should help anyone to get the plugin working.

Different Behavior Between Debug and Release Builds

I'm using the SOCKit library to implement a URL router for my app. I have a custom Router class that keeps track of all the valid routes and implements a match method that, given a route NSString, matches it to a corresponding view controller. To make things easier, the matchable view controllers must implement the Routable protocol, which requires an initWithState: method that takes an NSDictionary as a parameter. Here's the relevant code:
- (id)match:(NSString *)route
{
for (NSArray *match in routePatterns) {
const SOCPattern * const pattern = [match objectAtIndex:kPatternIndex];
if ([pattern stringMatches:route]) {
Class class = [match objectAtIndex:kObjectIndex];
NSLog(#"[pattern parameterDictionaryFromSourceString:route]: %#", [pattern parameterDictionaryFromSourceString:route]);
UIViewController<Routable> *vc;
vc = [[class alloc] initWithState:[pattern parameterDictionaryFromSourceString:route]];
return vc;
}
}
return nil;
}
When I run the app with the debug configuration, [pattern parameterDictionaryFromSourceString:route] produces what is expected:
[pattern parameterDictionaryFromSourceString:route]: {
uuid = "e9ed6708-5ad5-11e1-91ca-12313810b404";
}
On the other hand, when I run the app with the release configuration, [pattern parameterDictionaryFromSourceString:route] produces an empty dictionary. I'm really not sure how to debug this. I've checked my own code to see if there are any obvious differences between the debug and release builds to no avail and have also looked at the SOCKit source code. Ideas? Thanks!
I just ran into this issue myself today. The issue in my case was that Release builds blocked assertions, but in -performSelector:onObject:sourceString: and -parameterDictionaryFromSourceString: is this important line:
NSAssert([self gatherParameterValues:&values fromString:sourceString],
#"The pattern can't be used with this string.");
Which, when assertions are converted to no-ops, vanishes, and the gathering never happens. With no parameter values, not much happens! I changed it to the following (and will submit an issue to the GitHub repo):
if( ![self gatherParameterValues:&values fromString:sourceString] ) {
NSAssert(NO, #"The pattern can't be used with this string.");
return nil;
}
EDIT: reported as issue #13.

Displaying file copy progress using FSCopyObjectAsync

It appears after much searching that there seems to be a common problem when trying to do a file copy and show a progress indicator relative to the amount of the file that has been copied. After spending some considerable time trying to resolve this issue, I find myself at the mercy of the StackOverflow Gods once again :-) - Hopefully one day I'll be among those that can help out the rookies too!
I am trying to get a progress bar to show the status of a copy process and once the copy process has finished, call a Cocoa method. The challenge - I need to make use of File Manager Carbon calls because NSFileManager does not give me the full ability I need.
I started out by trying to utilize the code on Matt Long's site Cocoa Is My Girlfriend. The code got me some good distance. I managed to get the file copy progress working. The bar updates and (with some additional searching within Apple docs) I found out how to tell if the file copy process has finished...
if (stage == kFSOperationStageComplete)
However, I have one last hurdle that is a little larger than my leap right now. I don't know how to pass an object reference into the callback and I don't know how to call a Cocoa method from the callback once finished. This is a limit of my Carbon -> Cocoa -> Carbon understanding. One of the comments on the blog said
"Instead of accessing the progress indicator via a static pointer, you can just use the void *info field of the FSFileOperationClientContext struct, and passing either the AppDelegate or the progress indicator itself."
Sounds like a great idea. Not sure how to do this. For the sake of everyone else that appears to bump into this issue and is coming from a non-Carbon background, based mostly upon the code from Matt's example, here is some simplified code as an example of the problem...
In a normal cocoa method:
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
FSFileOperationRef fileOp = FSFileOperationCreate(kCFAllocatorDefault);
OSStatus status = FSFileOperationScheduleWithRunLoop(fileOp,
runLoop, kCFRunLoopDefaultMode);
if (status) {
NSLog(#"Failed to schedule operation with run loop: %#", status);
return NO;
}
// Create a filesystem ref structure for the source and destination and
// populate them with their respective paths from our NSTextFields.
FSRef source;
FSRef destination;
// Used FSPathMakeRefWithOptions instead of FSPathMakeRef which is in the
// original example because I needed to use the kFSPathMakeRefDefaultOptions
// to deal with file paths to remote folders via a /Volume reference
FSPathMakeRefWithOptions((const UInt8 *)[aSource fileSystemRepresentation],
kFSPathMakeRefDefaultOptions,
&source,
NULL);
Boolean isDir = true;
FSPathMakeRefWithOptions((const UInt8 *)[aDestDir fileSystemRepresentation],
kFSPathMakeRefDefaultOptions,
&destination,
&isDir);
// Needed to change from the original to use CFStringRef so I could convert
// from an NSString (aDestFile) to a CFStringRef (targetFilename)
CFStringRef targetFilename = (CFStringRef)aDestFile;
// Start the async copy.
status = FSCopyObjectAsync (fileOp,
&source,
&destination, // Full path to destination dir
targetFilename,
kFSFileOperationDefaultOptions,
statusCallback,
1.0,
NULL);
CFRelease(fileOp);
if (status) {
NSString * errMsg = [NSString stringWithFormat:#"%# - %#",
[self class], status];
NSLog(#"Failed to begin asynchronous object copy: %#", status);
}
Then the callback (in the same file)
static void statusCallback (FSFileOperationRef fileOp,
const FSRef *currentItem,
FSFileOperationStage stage,
OSStatus error,
CFDictionaryRef statusDictionary,
void *info )
{
NSLog(#"Callback got called.");
// If the status dictionary is valid, we can grab the current values to
// display status changes, or in our case to update the progress indicator.
if (statusDictionary)
{
CFNumberRef bytesCompleted;
bytesCompleted = (CFNumberRef) CFDictionaryGetValue(statusDictionary,
kFSOperationBytesCompleteKey);
CGFloat floatBytesCompleted;
CFNumberGetValue (bytesCompleted, kCFNumberMaxType,
&floatBytesCompleted);
NSLog(#"Copied %d bytes so far.",
(unsigned long long)floatBytesCompleted);
// fileProgressIndicator is currently declared as a pointer to a
// static progress bar - but this needs to change so that it is a
// pointer passed in via the controller. Would like to have a
// pointer to an instance of a progress bar
[fileProgressIndicator setDoubleValue:(double)floatBytesCompleted];
[fileProgressIndicator displayIfNeeded];
}
if (stage == kFSOperationStageComplete) {
NSLog(#"Finished copying the file");
// Would like to call a Cocoa Method here...
}
}
So the bottom line is how can I:
Pass a pointer to an instance of a progress bar from the calling method to the callback
Upon completion, call back out to a normal Cocoa method
And as always, help is much appreciated (and hopefully the answer will solve many of the issues and complaints I have seen in many threads!!)
You can do this by using the last parameter to FSCopyObjectAsync(), which is a struct of type FSFileOperationClientContext. One of the fields of that struct is info, which is a void* parameter that you can basically use as you see fit. Whatever you assign to that field of the struct you pass into FSCopyObjectAsync() will be passed in turn to your callback function as the last info function parameter there. A void* can be anything, including a pointer to an object, so you can use that to pass the instance of your object that you want to handle the callback.
The setup code would look like this:
FSFileOperationClientContext clientContext = {0}; //zero out the struct to begin with
clientContext.info = myProgressIndicator;
//All the other setup code
status = FSCopyObjectAsync (fileOp,
&source,
&destination, // Full path to destination dir
targetFilename,
kFSFileOperationDefaultOptions,
statusCallback,
1.0,
&clientContext);
Then, in your callback function:
static void statusCallback (FSFileOperationRef fileOp,
const FSRef *currentItem,
FSFileOperationStage stage,
OSStatus error,
CFDictionaryRef statusDictionary,
void *info )
{
NSProgressIndicator* fileProgressIndicator = (NSProgressIndicator*)info;
[fileProgressIndicator setDoubleValue:(double)floatBytesCompleted];
[fileProgressIndicator displayIfNeeded];
}