This is a Cocoa-component WebView. For example he called JS-function:
[webView stringByEvaluatingJavaScriptFromString:#"foo"];
I need to somehow wait until this function is executed and start doing another job. How this can be done on the Objective-C? I need something like:
[webView waitUntilJavaScriptCodeIsCompleted];
[webView stringByEvaluatingJavaScriptFromString:] is executed synchronously and will return an NSString of the result of executing the passed in script. So, if you just want to know that script has executed, call the function and when it returns, it has executed.
If, however, you're talking about a script with some sort of asynchronous execution, like an XMLHTTPRequest or setTimeout, then you're going to need to have your JavaScript call back into your Objective-C code to let it know when it's finished. Here is an example project that does just that. The most relevant parts are in AppDelegate:
#import "AppDelegate.h"
#import WebKit;
#import JavaScriptCore;
#interface AppDelegate ()
#property (weak) IBOutlet NSWindow *window;
#property (weak) IBOutlet WebView *webView;
#end
#interface AppDelegate (WebDelegate) <WebFrameLoadDelegate>
#end
#implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
JSGlobalContextRef jsGlobalContext = _webView.mainFrame.globalContext;
// Add a function named "allDone" to the window object. It can be called from JavaScript like so:
JSContext* context = [JSContext contextWithJSGlobalContextRef:jsGlobalContext];
context[#"allDone"] = ^{
NSLog(#"All Done was called");
};
context[#"jsLog"] = ^(NSString* message) {
NSLog(#"JSLOG: %#", message);
};
_webView.frameLoadDelegate = self;
NSURL* url = [NSURL URLWithString:#"https://example.com"];
NSURLRequest *req = [[NSURLRequest alloc] initWithURL:url];
[_webView.mainFrame loadRequest:req];
}
#end
#implementation AppDelegate (WebDelegate)
- (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame {
NSLog(#"Page loaded, calling JavaScript");
// Script that logs a message, and then invokes allDone after 2 seconds.
NSString* script =
#"jsLog('Script running');"
#"setTimeout(function() {\n"
#"jsLog('javascript timer fired, invoking allDone...');\n"
#"window.allDone();\n"
#"}, 2000);";
NSLog(#"Before stringByEvaluatingJavaScriptFromString");
[_webView stringByEvaluatingJavaScriptFromString:script];
NSLog(#"After stringByEvaluatingJavaScriptFromString");
}
#end
Which results in the following output:
Page loaded, calling JavaScript
Before stringByEvaluatingJavaScriptFromString
JSLOG: Script running
After stringByEvaluatingJavaScriptFromString
JSLOG: javascript timer fired, invoking allDone...
All Done was called
Related
I am quite new to react native and and the bridging mechanism with native code, especially when the framework has delegates. Assume I am trying to bridge the following framework:
#protocol BRPtouchNetworkDelegate;
#class PLNetworkModule;
#interface BRPtouchNetworkManager : NSObject <NSNetServiceBrowserDelegate,NSNetServiceDelegate>
#property(retain, nonatomic) NSMutableArray* registeredPrinterNames;
#property(assign, nonatomic) BOOL isEnableIPv6Search;
- (int)startSearch: (int)searchTime;
- (NSArray*)getPrinterNetInfo;
- (BOOL)setPrinterNames:(NSArray*)strPrinterNames;
- (BOOL)setPrinterName:(NSString*)strPrinterName;
- (id)initWithPrinterNames:(NSArray*)strPrinterNames;
- (id)initWithPrinterName:(NSString*)strPrinterName;
#property (nonatomic, assign) id <BRPtouchNetworkDelegate> delegate;
#end
#protocol BRPtouchNetworkDelegate <NSObject>
-(void) didFinishSearch:(id)sender;
#end
The following is the bridge module I implemented:
RCTBRPtouchNetworkManager.h
#import <React/RCTBridgeModule.h>
#import <BRPtouchPrinterKit/BRPtouchPrinterKit.h>
#interface RCTBRPtouchNetworkManager : NSObject <RCTBridgeModule, BRPtouchNetworkDelegate>
#end
RCTBRPtouchNetworkManager.m
#import "RCTBRPtouchNetworkManager.h"
#import <BRPtouchPrinterKit/BRPtouchPrinterKit.h>
#import <React/RCTLog.h>
#implementation RCTBRPtouchNetworkManager {
BRPtouchNetworkManager *_networkManager;
}
RCT_EXPORT_MODULE();
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location)
{
RCTLogInfo(#"Pretending to create an event %# at %#", name, location); //a dummy method to test the bridge
}
RCT_EXPORT_METHOD(startSearchWithTimeout:(int)time) {
RCTLogInfo(#"Bridge started search with time %d", time);
_networkManager = [[BRPtouchNetworkManager alloc] init];
_networkManager.delegate = self; //I'm setting delegate here
_networkManager.isEnableIPv6Search = NO;
NSString * path = [[NSBundle mainBundle] pathForResource:#"PrinterList" ofType:#"plist"];
if( path )
{
NSDictionary *printerDict = [NSDictionary dictionaryWithContentsOfFile:path];
NSArray *printerList = [[NSArray alloc] initWithArray:printerDict.allKeys];
[_networkManager setPrinterNames:printerList];
} else {
RCTLogInfo(#"PrinterList path not found");
}
// Start printer search
[_networkManager startSearch: 5.0];
}
- (void)didFinishSearch:(id)sender {
NSLog(#"didFinishedSearch"); //this delegate method is not called
}
#end
I can easily call the dummy method and see the results in the logs. However, the delegate method didFinishSearch() is never called. I call this from javascript as follows:
componentDidMount() {
let networkManager = NativeModules.BRPtouchNetworkManager;
networkManager.startSearchWithTimeout(5.0);
}
I there something I am missing? Am I implementing delegate properly? Is this kind of functionality even possible (can't seem to not since the delegate method was used by iOS community for a long time). Your help is much appreciated.
EDIT
I found that adding the following to my bridge manager file made the delegate to fire (thanks to this post)
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
However, even though this solves the problem, I'd like a more technical understanding on what is going on here since I can't seem to exactly grasp it. Thank you
I know this isn’t an an answer to the post but for the bit where you’ve asked for a more technical understanding - dispatch_get_main_queue(); puts the delegate method responses on to the main thread. Since JS is single threaded any process on the background thread won’t be visible to it.
I'm hoping someone can help as I'm new to iOS / objective C and very puzzled. I'm trying to play a simple sound using AVAudioPlayer as follows:
NSString *path = [[NSBundle mainBundle] pathForResource:#"soundFile" ofType:#"wav"];
NSURL *fileURL = [[NSURL alloc] initFileURLWithPath: path];
self.player=[[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:NULL];
[self.player play];
I am using ARC so I also have in my .h file, the following reference to my player so that ARC does not deallocate my player prematurely:
#property (nonatomic, strong) AVAudioPlayer *player;
This code works just fine and plays my sound PROVIDED that I run this code from a ViewController or my application's AppDelegate.
However if I cut and paste this very same code, plus all the necessary #includes and the #property and add them into another class in the same application but which is not a ViewController, and call the code there then no error is raised but no sound is played.
It is exactly the same code just called on a different class??
Why would it not work?
I have looked and looked for a similar post but nowhere have I seem exactly this scenario addressed. Many thanks if you can help me- would be much appreciated.
To clarify the issue-- here is how I call this code on another class say a class I have named Audio Tester, I would write in AppDelate say
#import "AppDelegate.h"
#import "AudioTester.h"
#interface AppDelegate ()
#end
#implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
AudioTester * tester = [[AudioTester alloc]init];
[tester playAudio];
}
where AudioTester playAudio is defined as
#import "AudioTester.h"
#implementation AudioTester
-(void) playAudio {
NSString *path = [[NSBundle mainBundle] pathForResource:#"soundFile" ofType:#"wav"];
NSURL *fileURL = [[NSURL alloc] initFileURLWithPath: path];
self.player=[[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:NULL];
[self.player play];
}
#end
with AudioTester.h as follows
#import <Cocoa/Cocoa.h>
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
#interface AudioTester : NSObject
#property (nonatomic, strong) AVAudioPlayer *player;
-(void) playAudio;
#end
Stepping through this code, it gets called just fine but it does not play sound?
If you can help that would be much appreciated. I'm totally stumped.
a little conceptual explanation about your code:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
AudioTester * tester = [[AudioTester alloc]init];
[tester playAudio];
}
1.
if you use ARC then the instance won't be not kept alive after the scope runs out, therefore the tester object will be immediately released, so in your case the object is deallocated before it could do anything – that is the reason why you can't hear any noise or sound.
if you want to keep your tester instance alive independently from your current scope where you inited in, you need to create like e.g. a property which is outside of the scope; you could put that into a class extension for instance:
#interface AppDelegate ()
// ...
#property (nonatomic, strong, nullable) AVAudioPlayer * tester;
// ...
#end
2.
we don't put anything like this into the AppDelegate.m file, the app delegate class basically handles the app-related events globally like launch, terminate, etc... briefly, the global and major events of the app's life cycle in runtime.
you can read more about its purpose in the official docs.
3.
you may use the –applicationDidFinishLaunching: method deliberately for initing your app, but I feel necessary to mention you may want to put everything inside the method –application:didFinishLaunchingWithOptions: instead.
you can read more about the initial procedure as well in the same documentation.
TL;DR
the answer to your original concern: NO, a class can be inited and instantiated in any other instance of any type of classes in general, but you need to worry about keeping the object alive as long as you want to use it.
In Header file
#import <WatchKit/WatchKit.h>
#import <Foundation/Foundation.h>
#import <WatchConnectivity/WatchConnectivity.h>
#interface InterfaceController : WKInterfaceController<WCSessionDelegate>
- (IBAction)lastSongButtonClick;
- (IBAction)playSongButtonClick;
- (IBAction)nextSongButtonClick;
#property (strong, nonatomic) IBOutlet WKInterfaceLabel *songTitleLabel;
#property (strong, nonatomic) IBOutlet WKInterfaceButton *playSongButton;
#end
So I implemented the WCSessionDelegate and every time I receive about the UI, I would want it to update. So in my .m file I have:
- (void)session:(nonnull WCSession *)session didReceiveMessage:(nonnull NSDictionary<NSString *,id> *)message{
NSString* type = [message objectForKey:#"type"];
if([type isEqualToString:#"UIUpdateInfo"]){
NSLog(#"Watch receives UI update info");
[self handleUIUpdateInfo:[message objectForKey:#"content"]];
}
}
AND
- (void)handleUIUpdateInfo:(NSDictionary*)updateInfo{
[self.songTitleLabel setText:[updateInfo objectForKey:#"nowPlayingSongTitle"]];
[self.playSongButton setBackgroundImage:[updateInfo objectForKey:#"playButtonImage"]];
}
However, it doesn't seems to update. Is there any proper way to update?
You're halfway there. You've configured receiving the message on the watch side correctly, but you'll need to trigger a message to be sent when the UI is updated (therefore triggering didReceiveMessage to execute and update the appropriate content).
Where ever you are making changes to the UI, you'll need to include this:
NSDictionary *message = //dictionary of info you want to send
[[WCSession defaultSession] sendMessage:message
replyHandler:^(NSDictionary *reply) {
//handle reply didReceiveMessage here
}
errorHandler:^(NSError *error) {
//catch any errors here
}
];
Also, make sure you're activating the WCSession properly. This is usually done in viewDidLoad or willAppear depending on whether you're implementing this on the phone or the watch.
- (void)viewDidLoad {
[super viewDidLoad];
if ([WCSession isSupported]) {
WCSession *session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];
}
}
You can see a full example of an end-to-end Watch to iPhone data transfer in this tutorial - http://www.kristinathai.com/watchos-2-tutorial-using-sendmessage-for-instantaneous-data-transfer-watch-connectivity-1
I have two different methods of trying to grab a variable from another class. The first one, which I would prefer using doesn't work - The second one, which I don't prefer does. Could someone please explain why?
AppDelegate.h
#import <Cocoa/Cocoa.h>
#interface AppDelegate : NSObject <NSApplicationDelegate> {
NSString *someString;
}
#property (assign) IBOutlet NSWindow *window;
#property (retain, nonatomic) NSString *someString;
- (void)manualSetVariable;
#end
AppDelegate.m
#import "AppDelegate.h"
#import "GrabFromAppDelegate.h"
#implementation AppDelegate
#synthesize someString;
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
someString = #"The Variable";
NSLog(#"In AppDelegate (1): %#",someString);
GrabFromAppDelegate *getThis = [[GrabFromAppDelegate alloc] init];
getThis.varSet = someString;
}
- (void)manualSetVariable { // THIS METHOD WORKS (2)
someString = #"The Variable";
NSLog(#"In AppDelegate(2): %#",someString);
}
#end
GrabFromAppDelegate.h
#import <Foundation/Foundation.h>
#interface GrabFromAppDelegate : NSObject {
NSString *varSet;
IBOutlet NSTextField *variable;
}
#property(retain, nonatomic) NSString *varSet;
- (IBAction)showVariable:(id)sender;
- (IBAction)manuallyGrabVariable:(id)sender;
#end
GrabFromAppDelegate.m
#import "GrabFromAppDelegate.h"
#import "AppDelegate.h"
#implementation GrabFromAppDelegate
#synthesize varSet;
- (IBAction)showVariable:(id)sender {
if (varSet != NULL) {
[variable setStringValue:[NSString stringWithString:varSet]];
NSLog(#"Got String Using (1): %#",varSet);
}
}
- (IBAction)manuallyGrabVariable:(id)sender { // THIS METHOD WORKS (2)
AppDelegate *getString = [[AppDelegate alloc] init];
[getString manualSetVariable];
if ([getString someString] != NULL) {
[variable setStringValue:[NSString stringWithString:[getString someString]]];
NSLog(#"Got String Using (2): %#",[getString someString]);
}
}
#end
This is horribly wrong:
AppDelegate *getString = [[AppDelegate alloc] init];
That allocates an AppDelegate instance, but it's not [[NSApplication sharedApplication] delegate], so the getString instance will never execute the applicationDidFinishLaunching method and your someString iVar will never get set. If you'd set someString within an init function, [[AppDelegate alloc] init] would, of course, call init. But since getString isn't attached to an NSApplication instance, there's nothing delegating the applicationDidFinishLaunching method to it.
If you want to get a pointer to the application delegate, you can do:
AppDelegate *getString = [[NSApplication sharedApplication] delegate];
or, for brevity's sake:
AppDelegate *getString = [NSApp delegate];
In second method you are calling method from app delegate and value is set at that time in someString variable and you get response. However if you are setting value for a varibale in appDelegate using an instance the set value will passed to that particular instance of controller class not to all instances. Hence either you create a shared instance of that variable or call that particular instance
to get the value somestring by showVariable method.
I am writing an application which will do a multiple task simultaneously. One particular task is to do a job once every 200 ms. To achieve this, I am using two methods calling each other. The first method just calls the second and the second method calls the first with a delay using dispatch_after().
After a few iterations(300-400 times) the block in dispatch_after is not executed after 200 ms. It takes ~5-10 seconds before the block is executed. Please let me know the reason for the behaviour(delay). I also tried NSThread (sleepForTimeInterval:) and I am facing the same problem there too.I am stuck. Please help me.
The code is given below.
Screen.h
#import <Foundation/Foundation.h>
#interface Screen : NSObject
-(void) firstMethod;
-(void) secondMethod;
#end
Screen.m
#import "Screen.h"
#implementation Screen
int i=0;
dispatch_queue_t another_queue;
dispatch_time_t pop_time;
-(Screen*) init {
self = [super init];
if (self) {
another_queue = dispatch_queue_create("com.test.timer.2", NULL);
}
return self;
}
-(void) firstMethod {
i++;
NSLog(#"i value : %d",i);
[self secondMethod];
}
-(void) secondMethod {
pop_time = dispatch_time(DISPATCH_TIME_NOW, 200 * NSEC_PER_MSEC);
dispatch_after(pop_time, another_queue, ^(void){
[self firstMethod];
});
}
#end
AppDelegate.h
#import <Cocoa/Cocoa.h>
#import "Screen.h"
#interface AppDelegate : NSObject <NSApplicationDelegate>
#property (assign) IBOutlet NSWindow *window;
#end
AppDelegate.m
#import "AppDelegate.h"
#implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
// Insert code here to initialize your application
Screen* screen = [[Screen alloc] init];
dispatch_queue_t first_queue = dispatch_queue_create("com.test.timer", NULL);
dispatch_block_t blk =^(void) {
[screen firstMethod];
};
dispatch_async(first_queue, blk);
}
#end
Is your app in the foreground when this occurs? If not, you may simply be seeing App Nap kicking in. One of the things it does is throttle timers in background apps.
One possible effect is "Timer Coalescing" and "App Nap".
Related: this question: Have you noticed that dispatch_after runs ~10% too slow on iOS devices?.
If this is actually the cause of your problem, you can fix it using a timer, either NSTimer, or use our own implementation, based on dispatch lib, where you can control the exact behavior. See also: an implementation of a timer on Gist: RXTimer
Edit:
On Mac OS X and when App Nap kicks in, it seems we have no control over the delay respectively that huge "leeway".
I had the same issue on OS X. My delay was more than 1000% off of the specified delay.
Turning off App Nap system wide solved the leeway:
defaults write NSGlobalDomain NSAppSleepDisabled -bool YES