UIWebView in UITableViewCell only completes load in first cell - javascript

I am using a UIWebView as the accessory view in each UITableViewCell in a UITable in order to display a tiny Openlayers map in each table view. When I first display the table view, the UIWebView in the first cell displays the map fine, but the other UIWebViews in all the other cells are empty.
If I scroll the table up and down, then the other cells all load fine when they come back into view, each with the respective different little maps.
The UIWebViews each load an initial HMTL document on init, and this document includes the standard Openlayers ol.js javascript.
The accessory view UIView subclass is the delegate of the UIWebView and waits for it to load, before sending it another javascript to draw the map features.
Logging in this delegate method reveals that the web view loaded property is YES/TRUE, but the javascript readyState is still loading.
Why would this be? How can I get this working as expected?
The webView is told to load the generic HTML/javascript via a call to loadHTMLString:baseURL: from within the drawRect: method. Then the webViewDidFinishLoad: delegate method is used to check when loading is finished, so that the cell-specific javascript can be run after the generic HTML/javascript is complete.
The relevant code is:
- (void)drawRect:(CGRect)rect {
NSString *path = [NSBundle pathForResource:#"style" ofType:#"html" inDirectory:[[NSBundle mainBundle] bundlePath]];
NSString *html = [NSString stringWithContentsOfFile:path usedEncoding:nil error:nil];
[_webView loadHTMLString:html baseURL:[NSURL fileURLWithPath:[path stringByDeletingLastPathComponent] isDirectory:YES]];
}
- (void)webViewDidFinishLoad:(UIWebView *)webView {
if ( ! _webView.loading ) {
if ( [[webView stringByEvaluatingJavaScriptFromString:#"document.readyState"] isEqualToString:#"complete"] ) {
NSLog(#"All good");
[self drawFeatures];
} else {
NSLog(#"ERROR: webView loaded, readyState '%#'", [webView stringByEvaluatingJavaScriptFromString:#"document.readyState"]);
//[self performSelector:#selector(drawFeatures) withObject:nil afterDelay:0.5];
}
}
}
( [self drawFeatures] assembles some javascript and sends it to the UIWebView to run, and it relies on the standard Openlayers (ol.js) javascript having completed in initial HTML web page.).
The output is:
2017-02-26 15:15:00.071 myapp[50443:25603667] ERROR: webView loaded, readyState 'loading'
2017-02-26 15:15:00.093 myapp[50443:25603667] ERROR: webView loaded, readyState 'loading'
2017-02-26 15:15:00.116 myapp[50443:25603667] ERROR: webView loaded, readyState 'loading'
2017-02-26 15:15:00.132 myapp[50443:25603667] ERROR: webView loaded, readyState 'loading'
2017-02-26 15:15:00.148 myapp[50443:25603667] ERROR: webView loaded, readyState 'loading'
2017-02-26 15:15:00.166 myapp[50443:25603667] ERROR: webView loaded, readyState 'loading'
2017-02-26 15:15:00.190 myapp[50443:25603667] ERROR: webView loaded, readyState 'loading'
2017-02-26 15:15:00.208 myapp[50443:25603667] ERROR: webView loaded, readyState 'loading'
2017-02-26 15:15:00.226 myapp[50443:25603667] ERROR: webView loaded, readyState 'loading'
2017-02-26 15:15:00.245 myapp[50443:25603667] ERROR: webView loaded, readyState 'loading'
2017-02-26 15:15:00.262 myapp[50443:25603667] ERROR: webView loaded, readyState 'loading'
2017-02-26 15:15:00.292 myapp[50443:25603667] All good
As you can see from this output, the isLoaded state for most of the cells is true BEFORE isLoaded is true for the FIRST CELL (which comes up last after really truly being loaded and actually ready). This implies that the web view thinks it's loaded when it's really not.
The loading error delegate method never gets called, so there appears to be no HTML/UIWebView errors:
-(void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
NSLog(#"ERROR loading style view HTML:\n%#", [error description]);
}
Note that if I change the delegate method to check only the javascript readyState, and ignore the isLoading value, I never get a readyState of complete. Just loading. Presumably, this is because the web view things it is finished loading, and so this delegate method doesn't get called again.
WORK AROUND THAT WORKS:
I can work around the problem by uncommenting the performSelector:withObject:afterDelay: that is commented out in the delegate method above. This indicates that the HTML does eventually load OK, but that the delegate method never gets called after it really is ready.
How can I get this to work as expected without resorting to a dodgy workaround of perform-with-delay?
UPDATE TRYING AN ALTERNATIVE METHOD
Here's another version of trying to get this working, based partly on the ideas from #Ricowere's answer (and similar to what I started with before going for the drawRect: hack
The HTML loading is now in my UIWebView subclass method. In this case, the appropriate method is when I set a property of that UIWebView subclass:
- (void)setStyle:(RenderStyle *)style {
_style = style;
...
...
NSString *path = [NSBundle pathForResource:#"style" ofType:#"html" inDirectory:[[NSBundle mainBundle] bundlePath]];
NSString *html = [NSString stringWithContentsOfFile:path usedEncoding:nil error:nil];
[self loadHTMLString:html baseURL:[NSURL fileURLWithPath:[path stringByDeletingLastPathComponent] isDirectory:YES]];
}
Then in the table view delegate, each time a table view cell is dequeued, add a UIWebView subclass view to it as an accessory view, and then for that UIWebView subclass view, call the property method that loads the HTML:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"styleCell"];
if ( cell == nil ) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#"styleCell"];
...
...
}
...
...
StyleViewUIWebViewSubclass *styleView = [[StyleViewUIWebViewSubclass alloc] initWithFrame:frame];
cell.accessoryView = styleView;
styleView.style = [[RenderStyle alloc] initWithString:styles[styleName]];
return cell;
}
However, this still has the same problem. The perform-with-delay work around above is still required to get it to work.

Firstly, remove the code you have in the drawRect. That approach is completely wrong, you're putting bussiness logic in a drawing method. (Which is executed once, and then the cells are reused)
Here, I show you a simple approach to tackle this.
class CellExample: UITableViewCell, UIWebViewDelegate {
var accessoryWebView: UIWebView? {
return accessoryView as? UIWebView
}
//If you're dequeuing the cell from IB.
override func awakeFromNib() {
// Set the frame you need or do this through autolayout.
let webView = UIWebView(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
accessoryView = webView
}
//You have to call this everytime a cell is dequeued
//(Then the content is properly displayed again.)
func load(url: URL) {
accessoryWebView?.delegate = self
//Load the request or the HTML content as you need.
//accessoryWebView?.loadHTMLString("whatever", baseURL: URL(string: "whatever")!)
// accessoryWebView?.loadRequest(URLRequest(url: url,
// cachePolicy: .returnCacheDataElseLoad,
// timeoutInterval: 10 ))
}
override func prepareForReuse() {
super.prepareForReuse()
//If the cell is reused 'restart' the stack.
accessoryWebView?.delegate = nil
accessoryWebView?.stopLoading()
}
func webViewDidFinishLoad(_ webView: UIWebView) {
// Do the stuff you need about the javascript.
}
}

Thanks to some tips from #Ricowere I got this working. My solution is based on the ideas from #Ricowere answer but using a UITableView delegate instead of a UITableViewCell subclass, and doing it all in Objective C instead of Swift.
Here's what ended up working for me...
In my WKWebView subclass:
- (id)initWithFrame:(CGRect)frame {
if ( self = [super initWithFrame:frame] ) {
...
self.navigationDelegate = self;
}
return self;
}
- (void)setStyle:(RenderStyle *)style {
_style = style;
NSURL *url = [NSBundle URLForResource:#"style" withExtension:#"html" subdirectory:nil inBundleWithURL:[[NSBundle mainBundle] bundleURL]];
[self loadFileURL:url allowingReadAccessToURL:[url URLByDeletingLastPathComponent]];
}
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
[self drawFeatures]; // This line runs the secondary instance-dependant javascript that relies on the generic javascript already having been completed
}
Then in my UITableViewDelegate:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"styleCell"];
if ( cell == nil ) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#"styleCell"];
...
WKWebViewSubclass *styleView = [[WKWebViewSubclass alloc] initWithFrame:frame];
cell.accessoryView = styleView;
}
...
((WKWebViewSubclass *)cell.accessoryView).style = [[RenderStyle alloc] initWithString:styleString];
return cell;
}
It seems to run a little slower than the UIWebView, which is odd, but at least it works without errors, and without any dodgy work arounds.

Related

WKWebView evaluateJavaScript is not working for youtube embedded video url

I've loaded a youtube embedded video to WKWebView:
https://www.youtube.com/embed/amtuB-2wGeQ?playsinline=1&autoplay=1
Then after WKWebView didFinishNavigation event is fired, I call:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("document.querySelector('video').play()", completionHandler: { (result, error) in
if let r = result {
print(r)
}
if let e = error {
print(e)
}
})
}
However, the javascript command is not being executed, both error and result are nil.
When I execute the same javascript command in Chrome developer tool, it successfully played the video and paused the video by calling "play()" and "pause()".
document.querySelector('video').play()
document.querySelector('video').pause()
I don't know what happened inside WKWebView, any ideas?
Thanks!
OK, finally figured out why it is not working.
If you are also struggling with "why my javascript is not working in WKWebView", below is a neat way to figure out why:
1. Open Safari on your desktop, Preferences -> Advanced, enable "show developer menu in menu bar"
2. In the Safari menu, Develop -> simulator, connect your iPhone simulator
3. Now you can see the web page html and script in Safari, in the console input area, try run different javascript to see which one works.
4. Then call the one that works from "evaluateJavaScript"
The reason why it didn't work from my case is that the embedded web page was rendered differently between common web browser and WKWebView. I took the browser script as example which didn't work.
With the help of Safari debugger, you can see what's really going on inside WKWebView, which is really cool.
Hope it helps if you ever run into the same problem.
Thanks!
Use UIWebView instead of WKWebView and use this code for play youtube video.
#pragma mark - Embed Video
- (UIWebView *)embedVideoYoutubeWithURL:(NSString *)urlString andFrame:(CGRect)frame {
NSString *videoID = [self extractYoutubeVideoID:urlString];
NSString *embedHTML = #"\
<html><head>\
<style type=\"text/css\">\
body {\
background-color: transparent;\
color: white;\
}\
</style>\
</head><body style=\"margin:0\">\
<embed id=\"yt\" src=\"http://www.youtube.com/v/%#\" type=\"application/x-shockwave-flash\" \
width=\"%0.0f\" height=\"%0.0f\"></embed>\
</body></html>";
NSString *html = [NSString stringWithFormat:embedHTML, videoID, frame.size.width, frame.size.height];
UIWebView *videoWebView = [[UIWebView alloc] initWithFrame:frame];
[videoWebView loadHTMLString:html baseURL:nil];
return videoWebView;
}
/**
#see https://devforums.apple.com/message/705665#705665
extractYoutubeVideoID: works for the following URL formats:
www.youtube.com/v/VIDEOID
www.youtube.com?v=VIDEOID
www.youtube.com/watch?v=WHsHKzYOV2E&feature=youtu.be
www.youtube.com/watch?v=WHsHKzYOV2E
youtu.be/KFPtWedl7wg_U923
www.youtube.com/watch?feature=player_detailpage&v=WHsHKzYOV2E#t=31s
youtube.googleapis.com/v/WHsHKzYOV2E
*/
- (NSString *)extractYoutubeVideoID:(NSString *)urlYoutube {
NSString *regexString = #"(?<=v(=|/))([-a-zA-Z0-9_]+)|(?<=youtu.be/)([-a-zA-Z0-9_]+)";
NSError *error = NULL;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:regexString options:NSRegularExpressionCaseInsensitive error:&error];
NSRange rangeOfFirstMatch = [regex rangeOfFirstMatchInString:urlYoutube options:0 range:NSMakeRange(0, [urlYoutube length])];
if(!NSEqualRanges(rangeOfFirstMatch, NSMakeRange(NSNotFound, 0))) {
NSString *substringForFirstMatch = [urlYoutube substringWithRange:rangeOfFirstMatch];
return substringForFirstMatch;
}
return nil;
}
or use google's youtube-ios-player-helper pod

WKWebview's javascript call returns nil

I'm trying to migrate from using a UIWebview to a WKWebview, and I need to invoke various Javascript functions that live in the webview.
In the UIWebview I have no trouble doing this, but the WKWebview throws an WKErrorDomain Error 4 for any and all calls. I thought it might be a race condition where I was calling functions that didn't exist in the DOM yet, but I added a debug statement to make those same calls once the page loaded and they're not working. I even did a 'document.body.innerHTML' which returned nil when there's clearly content being displayed in the webview.
The twist is that when inspecting the web code via Safari Web Inspector, everything works well and all calls work.
Why and how could this be? I do initialize the WKWebView with a WKWebViewConfiguration so that it'll share cookies across different webviews, so there might be something there, but I'm stumped.
This is how i initialize the WKWebView
WKWebViewConfiguration* config = [[WKWebViewConfiguration alloc] init];
config.processPool = [[WKProcessPool alloc] init];
WKUserContentController* userContentController = WKUserContentController.new;
WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource: #"document.cookie = 'cookie1=value1; domain=blah; path=/;';" injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
[userContentController addUserScript:cookieScript];
config.userContentController = userContentController;
WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectMake(0,0,50,50) configuration:config];
Here's a debugging statement where I log the document's innerHTML into the console log, which prints (nil) instead of the HTML.
NSLog(#"%#", [self.webView evaluateJavaScript:#"document.body.innerHTML" completionHandler:nil]);
WKWebView executes JS async unlike UIWebView; You cannot print like how you are doing..
Try this
[self.webView evaluateJavaScript:#"document.body.innerHTML" completionHandler:^(id result, NSError *error) {
NSLog(#"Error is %#",error);
NSLog(#"JS result %#" ,result);
}];

I would like to export the contents of ios web view as html

I am writing an app for ios to extract information from a webpage, however, the relevant pieces on the page are built by javascript. So when it is loaded by webview, the javascript is executed and the information displays no problem. If I try to load the page into a string by using the following method, the javascript is loaded, but not actually executed, therefore the string has no useful data in it.
NSData *urlData = [NSData dataWithContentsOfURL:[NSURL URLWithString:fullURL]];
NSString *responseString = [[NSString alloc] initWithData:urlData encoding:NSUTF8StringEncoding];
Is there another way besides loading the page into webview and exporting it from there? If not, how do you do that?
I'm not sure if there's another way outside of letting the UIWebView execute the JS and render the page, but if you do end up going this route, you could just grab the HTML of the whole page and pass that to the native end like so:
[dummyWebView stringByEvaluatingJavaScriptFromString:#"document.getElementsByTagName('html')[0].outerHTML;"];
Listening to the window.load event might be better to know when the page has finished going through all the JS
Good luck!
You set delegate to webView: self.webView.delegate = self; and implement UIWebViewDelegate:
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
NSString *html = [webView stringByEvaluatingJavaScriptFromString:#"document.documentElement.outerHTML"];
NSLog(#"html1 = %#", html);
// or use
NSString *html2 = [webView stringByEvaluatingJavaScriptFromString:#"document.body.innerHTML"];
NSLog(#"html2 = %#", html2);
}

Error on UIWebView with jQuery

I'm building an ePub reader and I'm loading the html files of every chapter on an UIWebView. When user reaches the end of a chapter, I load on the same webView the next chapter. As I need to execute some jQuery functions in order to create highlights, notes and many extra content, I'm also injecting the jQuery library to the viewer. This is the code I'm using:
- (void)loadBook{
...
[self.bookWebView loadRequest:[NSURLRequest requestWithURL:chapterURL]];
NSString *jQuery = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"jquery" ofType:#"js"] encoding:NSUTF8StringEncoding error:nil];
[self.bookWebView stringByEvaluatingJavaScriptFromString:jQuery];
}
When it loads the first chapter, when webViewDidFinishLoad: is called, $(document).readyfunction is called and all my jQuery code works perfectly. When I switch to the following chapter, the loadBook function is called again with the new URL but this time $(document).ready is not called so when I try to call any of my functions I get the following error: ReferenceError: Can't find variable: $
Why it is not loading jQuery library after my first call?
I ran into this today and Apple's documentation was to no avail. In my case, I was loading from local HTML, JS, and CSS files--NOT from a remote web address. So, this might not do it for you, but it does work for local files.
The following is a workaround. The code is in Swift.
Method:
In your UIWebViewDelegate, create an Int variable that will be incremented when your data is loaded.
Increment it by 1 each time that you load your data (or post the request)
In shouldStartLoadWithRequest, check whether the navigation type is "Other" or not: if it is and the counter variable is greater than 1, return false (NO in Objective-C).
This works because the view is instantiated each time it is presented, so the counter is reset to 0.
Code:
class MyView: ..., UIWebViewDelegate {
var theCounter = 0
...
func myFunctionWhereILoadTheData() {
... load the data (the code in your question)
theCounter += 1
}
...
func webView(webView: UIWebView!, shouldStartLoadWithRequest
request: NSURLRequest!, navigationType: UIWebViewNavigationType) -> Bool {
switch navigationType {
...
.Other:
if theCounter > 1: return false
}
}

webViewDidFinishLoad not calling when having javascript content in webpage

I'm using UIWebView in a project. Sometimes it doesn't call the webViewDidFinishLoad method. Because the web page have some javascripts. The method
- (BOOL) webView: (UIWebView *) webView shouldStartLoadWithRequest: (NSURLRequest *) request navigationType: (UIWebViewNavigationType) navigationType
is getting call but webViewDidFinishLoad doesn't. I want to catch that method. Because I'm start an animation when the webview start loading. Then I want to stop this animation when it finished. It's not working with websites having javascript content. Any one have an idea please?
Thanks
webViewDidFinishLoad method gets called when the UIWebView has finished loading the url, in your case since you are calling a javascript, it doesn't load your content, it just calls a javascript function in your already loaded webview. But YES you can catch the javascript actions in the same method you stated as below
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{
NSString *url = [[request URL] absoluteString];
if(navigationType == UIWebViewNavigationTypeLinkClicked){
if([url rangeOfString:#"SOMETHING"].length > 0 ){
//if your action returns something appended in your url like the anchors work
//DO YOUR STUFFS
}
}
return TRUE;
}
apparently, the webview in ios checks specifically for "iframe" tags in the html's dom and in case it finds, it creates events based on that tag as well. therefore, a simple however a patchy solution would be adding the following code in the part of the dom that is changing (for example, in the actual template of each section):
<iframe></iframe>
did the trick for me..

Categories

Resources