I decided I would change how I implement printing in my Cocoa app by having my app provide string-data to a webpage in an embedded webview, and then I would print that webview/frame.
The problem is that my code isn't being called and I don't see an error being returned.
Here is the setup:
1) Use Dashcode and build a webpage. There is no "form" container in the generated document, but it has fields like this:
<input id="customerNameField" type="text" name="" value="">
<input id="customerStreetField" type="text" name="" value="">
2) In IB, I create a window, toss in a WebView, link it to an outlet in my controller and create an NSURLRequest, grab my WebView's mainFrame and have it load the request. That works, the page is displayed in the WebView. Code:
[[wv mainFrame] stopLoading];
request = [NSURLRequest requestWithURL:currentPrintTemplateURL];
[[wv mainFrame] loadRequest:request];
3) I want to set the value of the customerNameField in my webView, so I do the following:
3A) Use a class-method on my controller that allows all keys to be accessible to the bridge:
+(BOOL)isKeyExcludedFromWebScript:(const char *)name {
// TODO: Implement specific blocks here; but for now let it all through
NSLog(#"Excluding nothing from WebScript");
return NO;
}
3B) Add a JavaScript function to my HTML file, which I want to call with arguments from Cocoa:
function populateRepairFields(repairCase) {
document.getElementById("customerNameField").value = repairCase;
}
(I know the javascript single-line-of-code works because I can add a button to my page, trigger the function in onClick and the code modifies the value of the customerNamefield.)
3C) Ask the webview for it's windowScriptObject, create an NSArray of arguments to be passed to the javascript function, and then execute them using the callWebScriptMethod method:
// grab the data we want to send to the javascript
NSString *customerFirstName = [self valueForKeyPath:#"currentRepairCase.customer.firstName"];
NSLog(#"(debug) Ensure we have a value - customerFirstName = %#", customerFirstName);
// build the array whose items will be passed as arguments to the javascript method
NSArray * args = [NSArray arrayWithObjects:customerFirstName, nil];
// get the windowScriptObject, then (debug) log the output so we can be sure it is not null
ws = [wv windowScriptObject];
NSLog(#"WebScriptObject = %#", ws);
//Then call the javascript method via the bridge, logging the output so we can see if there is an WSUndefined error returned (results are the same even if we don't wrap it in an NSLog)
NSLog(#"WS ERR: %#", [ws callWebScriptMethod:#"populateRepairFields" withArguments:args]);
4) All that. And .. nothing. I don't see the class method being called (3A), and don't get an WSUndefined or any other error message (3C), and I don't see an alert if I tried to add a javascript alert in my code.
I thought maybe I would need to setup a delegate for the WebView, but after checking the docs, I don't see a delegate requirement for WebScript. (I then connected all the delegate outlets to my controller, but that didn't help.)
Why is my code not seemingly being called? What's missing?
Thanks..
You can only use the WebScriptObject instance after the WebView instance is ready, and the WebView instance is not ready right after your call
[[wv mainFrame] loadRequest:request];
because the loading is done in the background thread automatically by WebKit.
Your code should work as is, as long as you call it shortly afterwards from the webview delegate, action methods, etc.
Related
I have a console program in C# with Selenium controlling a Chrome Browser Instance and I want to get all Links from a page.
But after the Page has loaded in Selenium the PageSource from Selenium ist different to the HTML of the Website I have navigated to. The Content of the Page is asynchronously loaded by JavaScript and the HTML is changed.
Even if I load the HTML of the Website like the following the HTML is still different to the one inside the Selenium controlled Browserwindow:
var html = ((IJavaScriptExecutor)driver).ExecuteScript("return document.getElementsByTagName('html')[0].outerHTML").ToString();
But why is the PageSource or the HTML returned by my JS still the same as it was when Selenium loaded the page?
EDIT:
As #BinaryBob has pointed out I have now implemented a wait-function to wait for a desired element to change a specific attribute value. The Code looks like this:
private static void AttributeIsNotEmpty(IWebDriver driver, By locator, string attribute, int secondsToWait = 60)
{
new WebDriverWait(driver, new TimeSpan(0, 0, secondsToWait)).Until(d => IsAttributeEmpty(d, locator, attribute));
}
private static bool IsAttributeEmpty(IWebDriver driver, By locator, string attribute)
{
Console.WriteLine("Output: " + driver.FindElement(locator).GetAttribute(attribute));
return !string.IsNullOrEmpty(driver.FindElement(locator).GetAttribute(attribute));
}
And the function call looks like this:
AttributeIsNotEmpty(driver, By.XPath("/html/body/div[2]/c-wiz/div[4]/div[1]/div/div/div/div/div[1]/div[1]/div[1]/a[1]"), "href");
But the condition is never met and the timeout is thrown. But inside the Chrome Browser (which is controlled by Selenium) the condition is met and the element has a filled href-Attribute.
I'm taking a stab at this. Are you calling wait.Until(ExpectedConditions...) somewhere in your code? If not, that might be the issue. Just because a FindElement method has returned does not mean the page has finished rendering.
For a quick example, this code comes from the Selenium docs site. Take note of the creation of a WebDriverWait object (line 1), and the use of it in the firstResult assignment (line 4)
WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
driver.Navigate().GoToUrl("https://www.google.com/ncr");
driver.FindElement(By.Name("q")).SendKeys("cheese" + Keys.Enter);
IWebElement firstResult = wait.Until(ExpectedConditions.ElementExists(By.CssSelector("h3>div")));
Console.WriteLine(firstResult.GetAttribute("textContent"));
If this is indeed the problem, you may need to read up on the various ways to use ExpectedConditions. I'd start here: Selenium Documentation: WebDriver Waits
I have built an app using titanium alloy
index.js
// Use the Alloy.Globals.Facebook namespace to make Facebook module API calls
var facebookModule = Alloy.Globals.Facebook;
//set facebook app id
facebookModule.appid = Ti.App.Properties.getString("ti.facebook.appid");
//set permissions i.e what data I want
facebookModule.permissions = ['user_friends','user_photos'];
// Do not force a facebook html popover but use the native dialog if possible
facebookModule.forceDialogAuth = false;
//invoke method onto button from module
$.fbButton.style = facebookModule.BUTTON_STYLE_WIDE;
$.index.open();
In my index.js controller I have this segment of code, it executes and I am presented with a log in screen.
I then fall into 2 problems:
1) "FB Session: Should only be used from a single thread"
2) I am unable to get the access token.
Not sure how to resolve both as the inbuilt login function has it's own event handler.
Cheers
Like you said, the inbuilt login function does have it's own handler.. so you should listen for event changes, something like this:
facebookModule.addEventListener('login', function(e) {
if (e.success) {
Ti.App.Properties.setString('face_token', facebookModule.getAccessToken());
// DO SOMETHING WITH THE TOKEN - open new window, auth the user...
}
});
If you try to get the access token BEFORE the login event is fired, you'll end up bad.
Now about the single thread thing.. I did run into this a while back.. I'm not sure exactly what I did to solve it, but I think it might be related to opening multiple windows or event allowing more than one call to the facebook API. Try to check if you are closing your windows and if the login function is being called more than once.
Let me know if that works for you. Good luck.
In my Phonegap Android app, I have the following Javascript code:
function onDeviceready()
{
window.plugins.webintent.getUri(function(url)
{
alert("WebIntent Fired Up! URL is " + url);
if (url.substring(0, 37) === "https://xxxxxxx.com/confirmation.html")
{
alert("intent matched!");
var params = url.substr(url.indexOf("?") + 1);
params = params.split("&");
var verificationData = params[0].split("=");
var emailData = params[1].split("=");
launchLinkEmail = emailData[1];
launchLinkVerification = verificationData[1];
alert("verification is " + launchLinkVerification);
alert("email is " + launchLinkEmail);
}
});
}
$(document).ready(function() {
document.addEventListener('deviceready', onDeviceready, true);
});
The problem is that the variables launchLinkVerification and launchLinkEmail seem to get set after the page is loaded and the Javascript is finishing up, and so their value is empty when I try to call it anywhere that I want to use them. The alerts always display the information I want, but if I try to display them anywhere in my HTML pages, or set conditionals based on their value, neither work.
On the other hand, it seems that if I use window.plugins.webintent.getUri(function(url) anywhere other than onDeviceready it sometimes doesn't execute at all (or at least not under conditions that I can predict or understand), and again the variables don't get set.
Ultmately, what I want to do is:
Get the data from the URL that WebIntent captures.
If the data from WebIntent matches certain criteria, then switch to another page using window.location = confirmation.html
Fill two fields on the form on confirmation.html with the two variables I got from the URL that WebIntent picked up.
How do I get the data from the Webintent call, switch pages depending on what that data is, and then use that data on the new page?
I haven't used the WebIntent plugin specifically, but if I understand your description correctly, I think you're running into a problem where you're running some JavaScript in the head or maybe in the body to configure the page the way you want it. But that code is dependent upon what happens in your onDeviceready(). The call to onDeviceready() is going to be made asynchronously at anytime PhoneGap feels it is ready. Usually it is called quickly, but quickly is a relative term.
What you likely need is someway for this async code to then trigger the code you want. JQuery provides the $.Deferred() object which you might find helpful. You can setup a Deferred, you add your other code in with Deferred.done(), and when it runs onDeviceready() resolves the object which then runs the callbacks.
http://api.jquery.com/jQuery.Deferred/
I've used this to allow something like onDeviceready() to trigger a series of other behaviors in my application which I may not have wanted to structure into one big function.
I've implemented what seems to be the only way of communicating from javascript to objective-c on iOS using the UIWebView delegate shouldStartLoadWithRequest() method.
It seemed to work fine at first, but now I notice that if I make multiple calls from javascript to objective-c within a short period of time, the second call is usually ignored (The application is a piano keyboard, each keypress triggers a call to native code, when dealing with multiple touches the native code doesn't get called for every finger).
This is my objective-c code to respond to javascript calls. It's pretty derpy I know but I just wanted something that works for now.
- (BOOL)webView:(UIWebView *)webView2 shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType
{
// Intercept custom location change, URL begins with "js-call:"
NSString * requestString = [[request URL] absoluteString];
if ([requestString hasPrefix:#"js-call:"])
{
// Extract the selector name from the URL
NSArray * components = [requestString componentsSeparatedByString:#":"];
NSString * functionCall = [components objectAtIndex:1];
NSArray * params = [functionCall componentsSeparatedByString:#"%20"];
NSString * functionName = [params objectAtIndex:0];
// Parse playnote event
if ([functionName isEqualToString:#"playNote"])
{
NSString * param = [params objectAtIndex:1];
NoteInstanceID note_id = [m_audioController playNote:[param intValue]];
NSString * jscall = [NSString stringWithFormat:#"document.PlayNoteCallback(%i);", note_id];
NSLog(#"playNote: %i", (int)note_id);
[m_webView stringByEvaluatingJavaScriptFromString:jscall];
}
// Parse stopnote event
if ([functionName isEqualToString:#"stopNote"])
{
NSString * param = [params objectAtIndex:1];
NoteInstanceID note_id = [param intValue];
NSLog(#"stopNote: %i", (int)note_id);
[m_audioController stopNote:note_id];
}
// Parse log event
if ([functionName isEqualToString:#"debugLog"])
{
NSString * str = [requestString stringByReplacingOccurrencesOfString:#"%20" withString:#" "];
NSLog(#"%s", [str cStringUsingEncoding:NSStringEncodingConversionAllowLossy]);
}
// Cancel the location change
return NO;
}
// Accept this location change
return YES;
}
From javascript I call objective-c methods by setting the src attribute of a single hidden iframe. This will trigger the delegate method in objective-c and then the desired native code will get called.
$("#app_handle").attr("src", "js-call:playNote " + key.data("pitch"));
app_handle is the id of the iframe in question.
To sum up, the basics of my method work, but multiple calls within a short period of time do not work. Is this just an artifact of the terrible method in which we are forced to communicate from javascript to objective-c? Or am I doing something wrong? I know PhoneGap does something similar to achieve the same goal. I'd rather not use PhoneGap, so if they don't have this problem then I'd love to figure out what they are doing to make this work.
Update:
I just found this: send a notification from javascript in UIWebView to ObjectiveC
Which confirms my suspicions about calls made in quick succession getting lost. Apparently I need to either lump my calls together or manually delay the calls so that the url request has returned by the time I make another call.
The accepted answer does not solve the problem since location changes that arrive before the first is handled are still ignored. See the first comment.
I suggest the following approach:
function execute(url)
{
var iframe = document.createElement("IFRAME");
iframe.setAttribute("src", url);
document.documentElement.appendChild(iframe);
iframe.parentNode.removeChild(iframe);
iframe = null;
}
You call the execute function repeatedly and since each call executes in its own iframe, they should not be ignored when called quickly.
Credits to this guy.
Rather than queuing everything on the JavaScript side, it's probably much easier and faster to move your complex logic (like the calls to the audio handler) off of the main thread using GCD. You can use dispatch_async() to queue up your events. If you put them into a serial queue, then they'll be certain to run in order, but you'll get back to the javascript faster. For instance:
Create a queue for your object during initialization:
self.queue = dispatch_queue_create("player", NULL);
In your callback:
if ([functionName isEqualToString:#"stopNote"])
{
NSString * param = [params objectAtIndex:1];
NoteInstanceID note_id = [param intValue];
NSLog(#"stopNote: %i", (int)note_id);
dispatch_async(self.queue, ^{[m_audioController stopNote:note_id]});
}
I need to know what should be done to use JavaScript in a HTML sitting in UIWebView to notify Objective-C that something has happened?
To be more exact, I'm playing some JavaScript animation in HTML and I need to alert the Objective-C code that the animation has ended.
There seems to be no official method of doing this. However, the standard workaround involves reading and parsing incoming URL requests, basically rolling your own serialized messaging protocol. The message handling should be done in the webView:shouldStartLoadWithRequest:navigationType method of your view controller.
Note: there are several free libraries (PhoneGap, QuickConnect, JS-to-Cocoa Bridge) which wrap this functionality (plus do a whole lot more). To reinvent the wheel (or know why it's round, so to speak), read on.
From JavaScript, you will invoke the callback by attempting to navigate to a new URL:
// In JavaScript
window.location = 'myapp:myaction:param1:param2'; // etc...
In Objective-C, implement the UIWebViewDelegate protocol in your .h file:
// In your header file
#interface MyAppViewController : UIViewController <UIWebViewDelegate> {
...
}
#end
Next, implement the method in your .m file:
// In your implementation file
-(BOOL)webView:(UIWebView *)webView2
shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType
{
// Break apart request URL
NSString *requestString = [[request URL] absoluteString];
NSArray *components = [requestString componentsSeparatedByString:#":"];
// Check for your protocol
if ([components count] > 1 &&
[(NSString *)[components objectAtIndex:0] isEqualToString:#"myapp"])
{
// Look for specific actions
if ([(NSString *)[components objectAtIndex:1] isEqualToString:#"myaction"])
{
// Your parameters can be found at
// [components objectAtIndex:n]
// where 'n' is the ordinal position of the colon-delimited parameter
}
// Return 'NO' to prevent navigation
return NO;
}
// Return 'YES', navigate to requested URL as normal
return YES;
}
Two important notes:
Context: navigating to myapp:whatever will (of course) fail under any other context. Keep this in mind if you're loading cross-platform pages.
Timing: if a second window.location = call is made before the first returns, it will get 'lost.' So, either lump your calls together, manually delay execution, or implement a queue which combines the above with JS queries into Objective-C objects.
Actually for timing in iOS (maybe not for OSX?), if a second window.location call is made before the previous window.location call executes, then the first window.location call gets lost. I think the window.location call executes asynchronisely with the JavaScript after it is called, and if another call is made it before it executes, it cancels the first.
For example, when capturing touch events, I have seen ontouchstart not get sent via window.location, if an ontouchmove event occurs to quickly afterwards (such as in a fast finger swipe). Thus your Objective-C doesn't get the ontouchstart event. This is more of a problem on the original iPad than the iPad2, I assume because of processing speed.
zourtney's answer is correct , but forgot to mention one thing .. needed to register delegate to webview by
- (void)viewDidLoad {
[super viewDidLoad]; --- instantiate _webview next .. then
_webview.delegate = self; //important .. needed to register webview to delegate
}
hope this helps .....
Swift Version
class ViewController: UIViewController,UIWebViewDelegate{
#IBOutlet weak var webviewInstance: UIWebView!
override func viewDidLoad() {
webviewInstance.delegate = self
super.viewDidLoad()
}
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
let requestString: String = (request.URL?.absoluteString)!
var components: [AnyObject] = requestString.componentsSeparatedByString(":")
// Check for your protocol
if components.count > 1 && (String(components[0]) == "myapp") {
// Look for specific actions
if (String(components[1]) == "myaction") {
// Your parameters can be found at
// [components objectAtIndex:n]
}
return false
}
return true
}
}