I have a issue that seems to have not been answered yet. I am new to cordova so I am not sure if this is a call in the framework. I have a a plugin which uses the Zbar libaray to scan barcodes. The resulting of the scan is managed by a delegate and is returning in the method imagePickerController:didFinishPickingMediaWithInfo: . My plugin calls the scan method but returns after my scan method ends. I need it to return the date to my website which requested it. I need to know how I can get get my scan method to wait for my zbar delegate to complete before sending a responds to my webview. Thank you in advance if you can solve this for me. and no the call [super writeJavascript:jsCallback] doesn't work, I am using cordova not phonegap.
#import "Camera.h"
#import <Cordova/CDV.h>
#import "AppDelegate.h"
#implementation Camera
#synthesize resultStr, command, hasPendingOperation;
//Override
- (void)pluginInitialize
{
NSLog(#"%#", #"init Camera");
}
- (void)scan:(CDVInvokedUrlCommand*)mycommand{
NSLog(#"%#", #"Camera.scan");
self.command = mycommand;
ZBarReaderViewController *reader = [ZBarReaderViewController new];
reader.readerDelegate = self;
reader.supportedOrientationsMask = ZBarOrientationMaskAll;
ZBarImageScanner *scanner = reader.scanner;
// TODO: (optional) additional reader configuration here
// EXAMPLE: disable rarely used I2/5 to improve performance
[scanner setSymbology: ZBAR_I25
config: ZBAR_CFG_ENABLE
to: 0];
// present and release the controller
[self.viewController presentViewController:reader animated:YES completion:nil];
NSLog(#"%#", #"finsihed plugin");
}
- (void) imagePickerController: (UIImagePickerController*) reader
didFinishPickingMediaWithInfo: (NSDictionary*) info
{
// ADD: get the decode results
id<NSFastEnumeration> results =
[info objectForKey: ZBarReaderControllerResults];
ZBarSymbol *symbol = nil;
for(symbol in results)
// EXAMPLE: just grab the first barcode
break;
self.resultStr = symbol.data;
// ADD: dismiss the controller (NB dismiss from the *reader*!)
[reader dismissViewControllerAnimated:YES completion:nil];
CDVPluginResult* pluginResult = nil;
if (self.resultStr != nil && [self.resultStr length] > 0) {
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:self.resultStr];
} else {
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:#"Failed to scan barcode!"];
}
NSLog(#"%#", self.resultStr); //<----- this is the date I need to return to my //webview this issue is scan: has already completed and returned
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}
#end
When you call the plugin from javascript, you provide a callback for success and failure. This callback is the only way for your plugin to communicate values back to your web layer.
Specifically, your javascript will look something like this.
cordova.exec(
function (result) {
// do stuff with plugin result! Hurray!
},
function (error) {
self.alert("Things went downhill, sorry... :\r\r" + error);
},
"PluginName", "MethodName", [parameters]
);
As the plugin result is already communicated through a callback, the fact that it is provided from a delegate should be irrelevant.
Update
Your question inquired about waiting on the Objective-C side. Do not wait. That's not how Cordova is designed, you'll even see warnings pop up in the console if you do not return immediately from the call.
Cordova plugin calls are designed to call asynchronous callbacks, and you have to design your web interface around those.
A common approach is to display a spinner or placeholder text while performing the call
Display spinner or placeholder text ("retrieving data")
Call plugin
In callback:
Remove spinner / placeholder.
Display result.
I discovered the issue, it has to do with the scope of the UIImagePickerController. This object is the only thing that is still within scope when the delegate method runs. I was mistakenly saving the callback ID as a class property, I thought I could retrieve it when the imagePickerController:didFinishPickingMediaWithInfo: method was called.
The fix was simply to extend the ZBAR reader class and add a property so I can store the call back ID. All Cordova plugins need the correct callback ID to return to the JS-side property.
Related
I am using a technique similar to the question linked below to capture window.print() and trigger printing of a WKWebView. This works fine for the main page.
Capturing window.print() from a WKWebView
What I am hoping to do is print an iFrame in the page, not the page itself. In JavaScript, I focus the iFrame and call window.print(), which works in Safari on iOS (which uses WKWebView?), and various desktop browsers.
Does anybody know how to use the WKWebView printFormatter to print an iFrame of HTML content?
i've been struggling with the exact same issue for the last 3 days, and was desperately looking across the web for a solution, which I didn't find.
But I finally got it running, so here is my solution for this:
Add the following user script to WKWebViewconfig.userContentController:
WKUserScript *printScript = [[WKUserScript alloc] initWithSource:#"window.print = function() {window.webkit.messageHandlers.print.postMessage('print')}"
injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:NO];
[webViewConfig.userContentController addUserScript:printScript];
[webViewConfig.userContentController addScriptMessageHandler:self name:#"print"];
=> this is the same as in Capturing window.print() from a WKWebview, with one crucial difference: forMainFrameOnly is set to NO, because we want to inject this in our iframe, and not only in the top window
Define a message handler in the view controller holding your WKWebview:
In the header:
#interface MyViewcontroller : UIViewController <UIGestureRecognizerDelegate, WKNavigationDelegate, WKScriptMessageHandler>
Implementation:
-(void) userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
Use the message handler to retrieve iframe's source (which is your pdf), and use-it as printing item for iOS's PrintController:
-(void) userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
if([message.name isEqualToString:#"print"])
{
[self.webView evaluateJavaScript:#"window.document.querySelector(\"iframe\").src" completionHandler:^(NSString *result, NSError *error)
{
UIPrintInteractionController *printController = UIPrintInteractionController.sharedPrintController;
printController.printingItem = [NSURL URLWithString:result];
UIPrintInteractionCompletionHandler completionHandler = ^(UIPrintInteractionController *printController, BOOL completed, NSError *error)
{
if(!completed)
{
if(error != nil) NSLog(#"Print failed: \nDomain: %#\nError code:%ld", error.domain, (long) error.code);
else NSLog(#"Print canceled");
}
NSLog(#"Print completed!\nSelected Printer ID: %#", printController.printInfo.printerID);
};
if(printController != nil)
[printController presentAnimated:YES completionHandler:completionHandler];
}];
}
else
NSLog(#"Unrecognized message received from web page"); }
NOTE: i use window.document.querySelector(\"iframe\") because our iframe does not have an id, otherwise I would have used window.getComponentByID. This important in case you have multiple iframes, which was not my case
I hope this can help you and others as well!
I am following the Spotify SDK tutorial, and trying to make a RN module for my application. This is my SpotifyModule.m code:
#import "SpotifyModule.h"
#import "React/RCTLog.h"
#import "React/RCTBridge.h"
#implementation SpotifyModule
RCT_EXPORT_MODULE()
+ (id)sharedManager {
static SpotifyModule *sharedManager = nil;
#synchronized(self) {
if (sharedManager == nil)
sharedManager = [[self alloc] init];
}
return sharedManager;
}
RCT_EXPORT_METHOD(authenticate:(RCTResponseSenderBlock)callback)
{
// Your implementation here
RCTLogInfo(#"authenticate");
self.auth = [SPTAuth defaultInstance];
// The client ID you got from the developer site
self.auth.clientID = #"8fff6cbb84d147e383060be62cec5dfa";
// The redirect URL as you entered it at the developer site
self.auth.redirectURL = [NSURL URLWithString:#"my-android-auth://callback"];
// Setting the `sessionUserDefaultsKey` enables SPTAuth to automatically store the session object for future use.
self.auth.sessionUserDefaultsKey = #"current session";
// Set the scopes you need the user to authorize. `SPTAuthStreamingScope` is required for playing audio.
self.auth.requestedScopes = #[SPTAuthPlaylistReadPrivateScope, SPTAuthUserReadPrivateScope];
//save the login callback
SpotifyModule *spotifyModule = (SpotifyModule *)[SpotifyModule sharedManager];
spotifyModule.loginCallback = callback;
//setup event dispatcher
spotifyModule.eventDispatcher = [[RCTEventDispatcher alloc] init];
[spotifyModule.eventDispatcher setValue:self.bridge forKey:#"bridge"];
// Start authenticating when the app is finished launching
dispatch_async(dispatch_get_main_queue(), ^{
[self startAuthenticationFlow];
});
}
- (void)startAuthenticationFlow
{
// Check if we could use the access token we already have
if ([self.auth.session isValid]) {
// Use it to log in
SpotifyModule *spotifyModule = (SpotifyModule *)[SpotifyModule sharedManager];
NSString *accessToken = self.auth.session.accessToken;
spotifyModule.loginCallback(#[accessToken]);
} else {
// Get the URL to the Spotify authorization portal
NSURL *authURL = [self.auth spotifyWebAuthenticationURL];
// Present in a SafariViewController
self.authViewController = [[SFSafariViewController alloc] initWithURL:authURL];
UIViewController *rootViewController = [UIApplication sharedApplication].delegate.window.rootViewController;
[rootViewController presentViewController:self.authViewController animated:YES completion:nil];
}
}
- (BOOL) application:(UIApplication *)app
openURL:(NSURL *)url
options:(NSDictionary *)options
{
// If the incoming url is what we expect we handle it
if ([self.auth canHandleURL:url]) {
// Close the authentication window
[self.authViewController.presentingViewController dismissViewControllerAnimated:YES completion:nil];
self.authViewController = nil;
// Parse the incoming url to a session object
[self.auth handleAuthCallbackWithTriggeredAuthURL:url callback:^(NSError *error, SPTSession *session) {
if (session) {
// Send auth token
SpotifyModule *spotifyModule = (SpotifyModule *)[SpotifyModule sharedManager];
NSString *accessToken = session.accessToken;
spotifyModule.loginCallback(#[accessToken]); }
}];
return YES;
}
return NO;
}
#end
The way I want to use it from the RN end, is call authenticate, with a callback for the access token. I got this working on Android fine.
Native.authenticate(function(token) {
store.dispatch(actions.loginSuccess(token));
});
On iOS, with the above code, I get to the attached screen, and when clicking Ok I get the following error:
SpotiFind[5475:29641] *** Terminating app due to uncaught exception
'NSInvalidArgumentException', reason: '+[SpotifyModule
application:openURL:sourceApplication:annotation:]: unrecognized
selector sent to class 0x10cb406f8'
So from my minimal ObjectiveC understanding, its trying to call a different method, than the one that the tutorial instructs to implement.
Any recommendations on how to make this work ?
If its any relevant, I build against iOS 10, and use the latest Spotify iOS SDK
p.s I realize the name might be against some copyrighting, its just temp for development :)
Thanks to your tips (in the comments), we managed to make our Spotify authentication work with React-native.
We used the code from your Pastebin to create a reusable module so that nobody has to waste time anymore.
You can find the module here: emphaz/react-native-ios-spotify-sdk
There is a tutorial for the setup and we even created a boilerplate project
Thanks a lot Giannis !
I have iPhone app made with PhoneGap 2.8.1 and I've recently faced strange problem:
I've written plugin that store some app secrets on device, hashes given data (using those secrets) and return this hash to JS.
When I run my app for the first time after installation everything work just fine.
But when I start it again app freezes just before (or maybe on) start of my plugin method.
3rd party plugins loaded before start ok, but when it comes to my plugin - nothing is happening.
Nothing on Xcode console, nothing on weinre console. Null. No error exceptions, no logs from native code.
When I try to run some native methods manually from weinre console nothing happens too.
But when I turn my app background by hitting home button and return to it, everything is magically starting to work!
Does anyone have similar problem? Any solutions? Clues?
I have method that hashes data which will be send in request to my API. I use it like this:
window.plugins.protocol.hamac data, (results) =>
# work with results
Native code looks like this:
- (void)hamac:(CDVInvokedUrlCommand*)command {
NSLog(#"HAMAC STARTED!");
CDVPluginResult* pluginResult = nil;
#try {
NSDictionary* data = [command.arguments objectAtIndex:0];
if (data != nil) {
// prepare string to be hashed
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary: #{
#"status": #YES,
#"h": [self sha1:[NSString stringWithFormat:#"%#", stringToHash]
}];
} else {
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary: #{
#"status": #NO,
#"error": #"No data!"
}];
}
} #catch (NSException* e) {
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary: #{
#"status": #NO,
#"error": #"Failed to hash data"
}];
}
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}
I'm using JSONModel to retrieve data from a simple webservice. I need to get the values of key #"message" into a mutable array.
- (void)viewDidLoad
{
[super viewDidLoad];
self.delegate = self;
self.dataSource = self;
NSString *conversationid = #"xx";
NSString *callURL = [NSString stringWithFormat:#"http://mydomain.com/inbox_messages.php?conversation=%#", conversationid];
_feed = [[MessageFeed alloc] initFromURLWithString:callURL
completion:^(JSONModel *model, JSONModelError *err)
{
self.messages = [[NSMutableArray alloc]initWithObjects:[_feed.messagesinconversation valueForKey:#"message"], nil];
NSLog(#"messages %#", self.messages);
}];
NSLog(#"test %#", self.messages);
}
The problem I'm experiencing is that while: NSLog(#"messages %#", self.messages); returns all the right data, NSLog(#"test %#", self.messages); returns (null).
The variable is declared in .h of my class as: #property (strong, nonatomic) NSMutableArray *messages;
This is probably a noob question but I'm a beginner and if somebody could give me a pointer in the right direction, I would be very happy.
Your NSLog for self.messages is outside of the completion block. The completion block is called after the data is loaded. The log is called immediately after creating the MessageFeed request. So, of course, the object self.messages is null because the request has not completed.
The solution to this would be to either handle all of your parsing within the completion block, or call another class method to parse the received data.
Your completion handler is being called after your NSLog("test %#", self.messages); is.
Blocks usually happen concurrently and when a certain event has occurred like the completion handler here or sometimes an error handler. By looking at your code you're probably getting something like:
test nil
messages
So your MessageFeed object is being run but it continues through your code and runs the NSLog outside of the completion handler scope first. When your JSON object has downloaded, which happens after, and parses it then runs the completion handler.
- (void)viewDidLoad
{
[super viewDidLoad];
self.delegate = self;
self.dataSource = self;
NSString *conversationid = #"xx";
NSString *callURL = [NSString stringWithFormat:#"http://mydomain.com/inbox_messages.php?conversation=%#", conversationid];
_feed = [[MessageFeed alloc] initFromURLWithString:callURL //this method takes some time to complete and is handled on a different thread.
completion:^(JSONModel *model, JSONModelError *err)
{
self.messages = [[NSMutableArray alloc]initWithObjects:[_feed.messagesinconversation valueForKey:#"message"], nil];
NSLog(#"messages %#", self.messages); // this is called last in your code and your messages has been has been set as an iVar.
}];
NSLog(#"test %#", self.messages); // this logging is called immediately after initFromURLWithString is passed thus it will return nothing
}
I am using GCDAsyncSocket (CocoaAsyncSocket) for the socket communication in my app. Due to the asynchronous nature of GCDAsyncSocket, my network request (submitMessage below) is decoupled from the callback block that runs when data is received (socket:didReadData).
- (void)submitMessage:(NSDictionary *)messageObject onCompletion:(completionBlock)block {
...
[_socket writeData:requestData withTimeout:self.timeout tag:0];
[_socket readDataToLength:4 withTimeout:self.timeout tag:TAG_HEADER];
}
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
...
NSDictionary *responseObject = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
if (self.completionBlock != nil)
self.completionBlock(responseObject);
}
}
This approach works fine for one-off exchanges. But there are some cases when I need to post a request, then using the received data, post another request. I can't get this to work properly. Basically, I need something like this:
[self submitMessage:request1 onCompletion:^(NSDictionary *response1) {
(...callback 1...)
}];
[self submitMessage:request2 onCompletion:^(NSDictionary *response2) {
(...callback 2...)
}];
}];
or
[self submitMessage:request1 onCompletion:^(NSDictionary *response1) {
(...callback 1...)
}];
[self submitMessage:request2 onCompletion:^(NSDictionary *response2) {
(...callback 2...)
}];
where the order is strictly request1 - callback1 - request2 - callback2.
So the question is, how can I block the second request to run after the callback of the first request? Would GCD (dispatch_sync?) be the way to go?
Edit
I ended up using a solution similar to what #tigloo suggested (hence accepting his answer), but using NSCondition instead of GCD (if anyone's interested in details, I followed this great discussion). I am already running multiple threads (UI in main, high-level socket comms in another thread, and the socket operations in a third thread). Setting a class property and using NSCondition to lock the GCDAsyncSocket delegate until the response arrive seems the cleanest approach.
I think you were almost there. What about
[self submitMessage:request1 onCompletion:^(NSDictionary *response1) {
// here, do something with response1 and create request2...
// then you can make request2 directly at the end of the callback:
[self submitMessage:request2 onCompletion:^(NSDictionary *response2) {
// here, do something with response2...
}];
}];
No need for the GCD directives, no need to block execution (which is a bad practice anyway). Does this solve your problem?
The easiest approach is to append your requests to a serial dispatch queue and then wait for them to be completed by using dispatch_sync(). A discussion on StackOverflow can be found here.
The actual way of implementing it is up to your preferences. A possible idea is the following:
Create a new class "SyncRequest"
This class ideally has a private property of type bool "requestFinished", initialized to NO in the class' init method
In a method such as "sendSyncRequest" you call submitMessage:completionBlock:
The completion block will set the "requestFinished" property to YES
The last line in "sendSyncRequest" will be dispatch_sync(syncRequestQueue, ^(void){while(!requestFinished);});
This way you can construct multiple instances of SyncRequest, each handling a synchronized request. Rough sketch implementation:
#interface SyncRequest
#property bool requestFinished;
#end
#implementation SyncRequest
dispatch_queue_t syncRequestQueue;
-(id)init
{
self = [super init];
if ( !self )
return nil;
self.requestFinished = NO;
syncRequestQueue = dispatch_queue_create("com.yourid.syncrequest", DISPATCH_QUEUE_SERIAL);
return self;
}
-(void) sendSyncRequest:(NSDictionary*)messageObject
{
// submit message here and set requestFinished = YES in completion block
// wait for completion here
dispatch_sync(syncRequestQueue, ^(void){while(!self.requestFinished);});
}
#end
NOTE: I wrote the code without having the compiler at hand, you may have to create an indirect reference to "self" in the dispatch_sync call in order to avoid a cyclic reference.