Objective-C calling javascript in UIWebView calling back Objective-C; threading issues - ios

I have a UIWebView-based application that is storing some state. I have a native tab bar and upper bar however. What I need is that when I click on the native bar, I persist the data stored in the UIWebView.
To do this, I called evaluateJavaScriptByString to create a JSON object and it does a callback to objective-C via the UIWebViewDelegate protocol.
However, I find that the callback is asynchronous and hence my main transaction happens before the data is loaded.
Any idea how this problem can be solved?
Update: To explain the problem better:-
I have an HTML page with a form that someone puts some data into.
My Tab bar is native.
On the click of a button on the tab bar, I want the data from HTML saved to an Objective-C model, and the view should change to some other view.
What is happening is that once I click the button, the javascript call happens to the page (to create a JSON string to send to Objective-C) and this data does get saved to the obj-c model. However, this happens asynchronously. So my screen changes before the data is loaded into the model. If I refresh the next screen it shows the correct data. I was wondering if there was a way around this.
Note, I know how to call OBjective-C functions from WebViews. We are using JSOBjBridge for that anyway.

Just add something like this #"some_var = MAKE_JSON(); window.location = \"myapp://callback/\" + escape(some_var)" at the end of your javascript code, that you transfer to evaluateJavaScriptByString:
In UIWebView's delegate implement webView:shouldStartLoadWithRequest:navigationType: and catch all urls with myapp:// prefix like this:
- (BOOL) webView: (UIWebView *) webView
shouldStartLoadWithRequest: (NSURLRequest *) request
navigationType: (UIWebViewNavigationType) navigationType {
NSString *url = #"myapp://";
NSString *path = request.mainDocumentURL.relativePath;
NSString *callbackURL = #"callback/";
if ( [path hasPrefix:url] ) {
path = [path substringFromIndex:[url length]];
if ( [relPath hasPrefix:callbackURL] ) {
NSString *json = [path substringFromIndex:[callbackURL length]];
//TODO: Work with json
}
return NO;
}
return [super webView:webView shouldStartLoadWithRequest:request navigationType: navigationType];
}
I didn't test this code, I just written it from scratch, but it should work, I already did it in this way.

Related

How to manipulate iFrames in a WKWebView?

I want to inject custom headers into NSURLRequests made for top-level frames of WKWebView and all of its iFrames.
To achieve this I am listening to the
- webView:decidePolicyForNavigationAction:decisionHandler: method. In this method i want to cancel the existing page load and create a new NSURLRequest and reload the page.
Here's the code snippet I am using,
- (void)webView:(WKWebView*)webView
decidePolicyForNavigationAction:(WKNavigationAction*)action
decisionHandler:
(void (^)(WKNavigationActionPolicy))decisionHandler {
NSString* header = #"Custom-Header";
if([action.request.allHTTPHeaderFields objectForKey:header] ){
NSLog(#"Header already added for URL - %#",action.request.URL.absoluteString );
}
else{
decisionHandler(WKNavigationActionPolicyCancel);
NSLog(#"Adding header for URL %#",action.request.URL.absoluteString);
NSMutableURLRequest *mutableRequest = [action.request mutableCopy];
NSString* headerVal;
if(action.targetFrame.mainFrame)
{
headerVal = #"MAINFRAME";
}
else
{
headerVal = #"SUBFRAME";
}
[mutableRequest addValue:headerVal forHTTPHeaderField:header];
[action.targetFrame.webView loadRequest:mutableRequest];
return;
}
// further processing
}
However with this approach I face the following problem - If the callback is received for a iFrame, the entire page reloads with the iFrame URL. Instead I want only the iFrame to reload with the URL. Is there any way to solve this?
Note that at the end of this I want to be able to send a custom header indicating whether the request originated from the main frame or an iFrame.Please do suggest if there are better solutions to achieve this.

How to determine why WKWebView is crashing

I am using WKWebView in a Swift app built for iOS 8+. I use instances of WKWebView in a variety of views in my app, e.g. in each of the tabs in my tab view controller, the interface is based on WKWebView.
I and my testers have noticed these views sometimes go completely blank, and after researching that issue, it seems that WKWebView can crash, and the blank view is the result. Luckily, it doesn't bring down the app due to the way WKWebView operates, but I also am not clear on how to trap/log information about what caused it to crash (if that is actually what is happening).
How can I determine if/why a WKWebView has crashed?
My current workaround for the issue is that I use KVO (actually, Facebook's KVOController), to monitor the "URL" property of the WKWebView, and if it goes from non-nil to nil, I assume a crash has happened, and I reload the webview:
kvoController?.observe(webView, keyPath: "URL", options: NSKeyValueObservingOptions.New|NSKeyValueObservingOptions.Old) { (areaViewController, webView, change) -> Void in
if change[NSKeyValueChangeNewKey] is NSNull && !(change[NSKeyValueChangeOldKey] is NSNull) {
areaViewController.setup() // reload our webview
}
}
But obviously it would be nice to figure out the root cause of the crash.
This is very nasty WKWebView issue that we also encountered in Firefox for iOS.
See the following bug report https://bugs.webkit.org/show_bug.cgi?id=148685
I have no great solution but I do have two tips:
First, we were also doing the reload when we found out via KVO that webView.URL turned nil. However it turns out that it turns nil on a number of events. For example when a redirect happens and possibly also when a form is submitted. So this is not ideal.
Second, in iOS9 there is a new API to detect when the WebKit content process has died. We have not tried this yet but I think that will be a better trigger to reload the webView.
There is no documentation yet I think, but you can see this in the header files:
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView NS_AVAILABLE(10_11, 9_0);
(void)webViewWebContentProcessDidTerminate:(WKWebView *)webView NS_AVAILABLE(10_11, 9_0); This delegate getting called only in iOS9.but if you reload webview ,you will lost all of the state information stored.
maybe can save info befor called [webview reload]
for example:
NSString *jsString = #"var arry=[];
for(var i=0; i<sessionStorage.length; i++){
var a={};a[sessionStorage.key(i)]=sessionStorage.getItem(sessionStorage.key(i));arry.push(a)};arry";
[webView evaluateJavaScript:jsString completionHandler:^(id _Nullable obj, NSError * _Nullable error) {
NSLog(#"jsString1===error=%#",error);
NSLog(#"jsString1==obj==®%#",obj);
if (obj == NULL ||obj ==[NSNull class]||obj==nil) {
//重启webview
[webView reload];
return ;
}
//保存sessionStorage
NSArray *arr = (NSArray*)obj;
//保存后重启webview
[webView reload];
}];

UIWebView screenshot

I'm facing a problem trying to take screenshots form UIWebViews. I need to take some screenshots of my UIWebView and it works but the screenshot is not correct because they are taken in the event webViewDidFinishLoad, but it calls webViewDidFinishLoad when the UIWebView is not loaded fully, I mean, I take the screenshot in the event webViewDidFinishLoad but the UIWebView is not correctly and fully loaded so it takes a screenshot and it makes right but the screenshot is not totally correct because its the event is triggered (webViewDidFinishLoad) but the UIWebView is not totally loaded. Any ideas?
Thank you very much
A user was having a issue with their use of a activity indicator that apears until a page has loaded but theirs would come back after the main content had loaded the question is here UIWebView not finishing loading? and the answer code is used below. After the code I will go into detail about how this could be used for your situation.
(In the code below the web page truly starts loading on //show UIActivityIndicator and truly finishes loading the main content (not the extra content you are struggling with) on //hide UIActivityIndicator)
//Define the NSStrings "lastURL" & "currentURL" in the .h file.
//Define the int "falsepositive" in the .h file. (You could use booleans if you want)
//Define your UIWebView's delegate (either in the xib file or in your code)
- (void)webViewDidFinishLoad:(UIWebView *)webView {
lastURL = [[NSString alloc] initWithFormat:#"%#", webView.request.mainDocumentURL];
if (falsepositive != 1) {
NSLog(#"Loaded");
//hide UIActivityIndicator
} else {
NSLog(#"Extra content junk (i.e. advertisements) that the page loaded with javascript has finished loading");
//This method may be a good way to prevent ads from loading hehe, but we won't do that
}
}
-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType; {
NSURL *requestURL =[request mainDocumentURL];
currentURL = [[NSString alloc] initWithFormat:#"%#", requestURL]; //not sure if "%#" should be used for an NSURL but it worked...
return YES;
}
- (void)webViewDidStartLoad:(UIWebView *)webView {
if ([currentURL isEqualToString:lastURL]) {
falsepositive = 1;
NSLog(#"The page is loading extra content with javascript or something, ignore this");
} else {
falsepositive = 0;
NSLog(#"Loading");
//show UIActiviyIndicator
}
}
When a page loads in iOS, webviewdidstart and webviewdidfinish are called but not knowing the difference in your main page/html content objective-c calls this again for extra content such as ads or frames. Using this if I were in your situation I would create a BOOL such as pageIsLoading and set it to true in webviewdidstart and then set it to false in webviewdidfinish. After the BOOL is turned off in webviewdidfinish I would end webviewdidfinish by calling a method that will check after a short delay if the BOOL pageIsLoading == YES and if it is, do nothing because more content is loading. If pageIsloading == no then all content must be loaded and now would be a good time to take your snapshot.
Rather than taking screenshot in the event webViewDidFinishLoad you can try to take the screenshot explicitly using UIButton. Let this button be disabled and you can just set this button's enabled property to YES after web view fully loads its content.
On button click to can implement your code to take screenshot.
Edited : Webview's webViewDidFinishLoad method is called when it finishes loading its content. Might be its possible that it may be taking some time to render some images/content on its view.
If possible you can use NSTimer in your webViewDidFinishLoad method to wait for sufficient time(1 min) so that mean while webview can load its content fully.
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:60.0
target: self
selector: #selector(MethodName:)
userInfo: nil
repeats: NO];
In Selector method you can implement your code of taking screenshot.
Hope this will help you...

Can a UIWebView interact (communicate) with the app?

I went to use UIWebView to display dynamic content, instead of doing it natively using UI-elements. Is it possible to trigger native app functions from simply hitting links inside the UIWebView? Example: hitting a link which then switches current View?
Yes, it's possible. In your html, you write a JS to load a URL with a fake scheme such as
window.location = "request_for_action://anything/that/is/a/valid/url/can/go/here";
Then, in your iOS code, assign a delegate to your webView, and in your delegate, handle
webView:shouldLoadWithRequest:navigationType
with something like
if( [request.URL.scheme isEqualToString: #"request_for_action"] )
{
// parse your custom URL to extract parameter, use URL parts or query string as you like
return NO; // return NO, so webView won't actually try to load this fake request
}
--
Just an aside, you can do the other way, let iOS code invoke some JS codes in your html by
using
NSString* returnValue = [self.webView stringByEvaluatingJavaScriptFromString: "someJSFunction()"];
Yes! When the user presses a link, you hear about it in the web view's delegate and can then do whatever you want. Powerful stuff can be done this way.
The web view's delegate is sent webView:shouldStartLoadWithRequest:navigationType:. You analyze what happened, and respond as you wish. To prevent the web view from trying to follow the link (which may be completely fake, after all), just return NO.
In this example from the TidBITS News app, I have a link in the Web page that uses a totally made-up play: scheme. I detect that in the delegate and play:
- (BOOL)webView:(UIWebView *)webView
shouldStartLoadWithRequest:(NSURLRequest *)r
navigationType:(UIWebViewNavigationType)nt {
if ([r.URL.scheme isEqualToString: #"play"]) {
[self doPlay:nil];
return NO;
}
if (nt == UIWebViewNavigationTypeLinkClicked) {
[[UIApplication sharedApplication] openURL:r.URL];
return NO;
}
return YES;
}
Implement the UIWebViewDelegate method webView:shouldStartLoadWithRequest:navigationType:.
Handle navigationType and the request as needed.

Problems using UIWebView to implement info page

In my app I have a UIWebView-based view controller showing info and credits about the app itself. As part of the info, the app version is displayed, as retrieved from the infoDictionary of the mainBundle. So this string is not in the original HTML; it is set programmatically, by replacing a placeholder.
Therefore the sequence is:
1) I load the HTML into a NSString
2) I replace the placeholder with the actual version
3) I show the resulting string in UIWebView by invoking method loadHTMLString:baseURL:
The HTML also has hyperlinks to some web pages in the internet.
Everything is fine, but for this problem:
If I touch a hyperlink, and therefore navigate to the corresponding web page, I will not be able to go back to my original info page (canGoBack returns NO, goBack does nothing).
Any suggestion?
Thanks in advance.
In the end, I have found a satisfactory solution, that I would like to share.
1) I changed the HTML code of my info page, by inserting the following code where I want to show the app version:
<p>App version:
<script type="text/javascript">
var args = document.location.search.substring(1).split("&");
for (var i in args) {
var nameValue = args[i].split("=");
if (nameValue[0] == "version")
var version = nameValue[1];
}
if (version)
document.write(version);
</script>
</p>
This piece of code will parse the query part of the URL, looking for a "version=<version>" argument, and use its value to show the app version.
2) When preparing the URL to be used by the UIWebView, I retrieve the app version from the main bundle:
NSString* version = [[[NSBundle mainBundle] infoDictionary] objectForKey:#"CFBundleVersion"];
Then I append it at the end of my URL string:
NSString* urlString = [NSString stringWithFormat:#"<my_info_page>.html?version=%#", version];
3) Finally, I use the UIWebView to show the page:
NSURL* url = [[NSURL alloc] initWithString:urlString];
NSURLRequest* request [[NSURLRequest alloc] initWithUrl:url];
[mWebView loadRequest:urlRequest];
[urlRequest release];
[url release];
where mWebView is an IBOutlet, of type UIWebView, properly connected to the web view in the XIB file.
It works correctly on my iPhone 4, including the back/forward functions of the web view when following hyperlinks, while keeping the user inside the app.
I would break out of the app and load Safari in this case:
- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType {
[[UIApplication sharedApplication] openURL:request.URL];
return NO;
}
The only downside is that the user comes out of your app. You could put an alertview in that method to warn them first...
Alternatively, you'll have to code your own back button.

Resources