I'm building a table with some text that is HTML, so I am using a UIWebView as a subview of my custom table cells. I already ran into one problem - as I scrolled down in the table, it would take the UIWebViews a second to update. For example, I'd be viewing Cells at rows numbered 1, 2, and 3. I'd scroll down to say 8, 9, and 10. For a moment, the content of the UIWebView that was visible in cell #8 was the content from cell #1, the content in cell #9 was that from cell #2, and so on.
I learned that the problem was that UIWebViews simply render their text slowly. It was suggested to instead preload the content into the UIWebView as soon as I could instead of waiting until the table receives the cellForRowAtIndexPath. So now, I have a Domain Object which before just had the text content of the WebView - but now it actually has a reference to the UIWebView itself.
But now some of the content in the UIWebView renders, and when I scroll through the table the UIWebView shows only as a grey box. If I touch the grey box, it will actually receive the touch and update the WebView - for example if I touch a link (which I may or may not do, since the box is gray and it would be by a stroke of luck), the page that was linked to will be requested and displayed properly.
- (id)initWithFrame:(CGRect)frame reuseIdentifier:(NSString *)reuseIdentifier {
if (self = [super initWithFrame:frame reuseIdentifier:reuseIdentifier]) {
// I suppose this isn't necessary since I am just getting it from the
// Domain Object anyway
content = [[UIWebView alloc] initWithFrame:CGRectZero];
content.backgroundColor = [UIColor blackColor];
[self addSubview:content];
[content release];
}
return self;
}
// called by cellForRowAtIndexPath
- (void)setMyDomainObject:(MyDomainObject*)anObject {
UIWebView *contentWebView = anObject.contentWebView;
int contentIndex = [self.subviews indexOfObject:content];
[self insertSubview:contentWebView atIndex:contentIndex];
}
One way to deal with this would be to use several UILabels, each with different fonts. Then strategically place them within the cell as to make them seem like contiguous text. I've seen this done in a UITableView with very good results.
Another performance problem may be that you are overiding UItableViewCell and it's init method. This almost always leads to poor performance in a UITableView. Instead just use the regular UITableViewCell instances and add subviews to it's contentView.
This has been covered extensively by Matt Gallagher's Easy Custom UITableView Drawing.
Another method described was to use Three20, which can give you styled text as well.
What you are attempting currently can't be done with UIWebview.
Preloading won't help: this would slow down the UI when UIWebviews are being rendered offscreen; you should always practice lazy loading on the iPhone.
Putting UIWebviews in a UITableView will come with a potentially unacceptable performance hit. You will need to use other means to accomplish your goal.
Unfortunately, NSAttributedString is unavailable in UIKit, which could easily solve your problem.
So here's my updated answer: I've implemented a table using one big UIWebView to put styled text inside table cells in my own Twitter client app, HelTweetica (with source available). It works pretty well as long as you understand how to do layout in HTML and CSS.
You just create a NSMutableString and add the lines of HTML that make up your document. You can references external files such as CSS files that are in your app's sandbox. You can set custom URL actions such as "youraction://file.txt" that your web view delegate can intercept and process as a replacement IBActions:
- (void) refreshWebView {
TwitterAccount *account = [twitter currentAccount];
NSMutableString *html = [[NSMutableString alloc] init];
// Open html and head tags
[html appendString:#"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n"];
[html appendString:#"<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\" lang=\"en\">\n"];
[html appendString:#"<head>\n"];
[html appendString:#"<meta name='viewport' content='width=device-width' />"];
[html appendString:#"<link href='style-ipad.css' rel='styleSheet' type='text/css' />"];
[html appendString:#"<script language='JavaScript' src='functions.js'></script>"];
// Body
[html appendString:#"</head><body><div class='artboard'>"];
// [...]
// Close artboard div, body. and html tags
[html appendString:#"</div></body></html>\n"];
NSURL *baseURL = [NSURL fileURLWithPath: [[NSBundle mainBundle] bundlePath]];
[self.webView loadHTMLString:html baseURL:baseURL];
[html release];
}
And you can reload parts of the page by using javascript:
- (void) refreshTabArea {
NSString *html = [self stringByEscapingQuotes: [self tabAreaHTML]];
NSString *js = [NSString stringWithFormat: #"document.getElementById(\"tab_area\").innerHTML = \"%#\";", html];
[self.webView stringByEvaluatingJavaScriptFromString:js];
}
If the content in the web view is restricted to styled text or hyperlinks you might want to take a look at the Three20 project: http://github.com/joehewitt/three20/tree/master
Its TTStyledText class has support for <b>, <i>, <img>, and <a> tags in the content. Probably more lightweight than webviews.
This isn't answering the original question asked, but taking one step back and looking at the bigger picture, if you're trying to display a hyperlink in a table cell, does that mean when you click on it it opens a web browser? Would it be the same if you showed styled text in the table cell that looks like or hints at a link, but open a separate screen with a full-screen web view that lets you tap on the link?
You said 'called by setRowAtIndexPath', you might mean 'cellForRowAtIndexPath' which is a UITableView method called when a row becomes visible and needs to create a cell. Make sure that in this method you are properly initializing and updating the cell contents.
Have you looked into overriding the -prepareForReuse method on your table cell subclass? If the cells don't seem to update when you scroll, it's possible that the content of reusable cells isn't being cleared and reset.
My understanding is that a WebView won't render unless you initiate the loading of it AFTER viewDidLoad() happens.
Related
I have a UIView which, on viewDidLoad creates and shows a UIWebView. The web view doesn't actually show its contents until the view has actually appeared though. The navigation bar appears with the view during its animation process but the web view is populated a second after the animation has finished. How can I make the web view load the contents BEFORE animation begins. Here is my code, thanks:
-(void)viewDidLoad
{
[super viewDidLoad];
self.title = #"Help";
NSString *html = #"my html contents goes here, all local and all within a string - does contain one BG image though";
UIWebView *webView = [[UIWebView alloc] initWithFrame:_webFrame];
NSString *path = [[NSBundle mainBundle] bundlePath];
NSURL *baseURL = [NSURL fileURLWithpath:path];
[webView loadHTMLString:html baseURL:baseURL];
[self.view addSubView:webView];
[webView release];
}
I think a UIWebView needs some time to initialize, since it uses the WebKit engine.
You could try adding a simple timeout before actually pushing the new viewcontroller or show a placeholder during the 'push' animation.
As far as I know, a UIWebView has to be on screen in order to load and render.
i'd try the following:
create and add web view on the calling view controller.
set frame of the web view in a way only one pixel is shown on screen.
load html content.
push to new ViewController, remove Web View from old parent and add to new one.
might work, but it's really not worth the hassle, why not try to "solve" the issue by adding a load indicator or a placeholder text?
I'm trying to show TXT file (ASCII) into UIVewView. For example, using the site www.partisani.net/35.txt in Safari on MacBook works fine, Safari on iPhone doesn't. Safari's iPhone shows the file with another layout. Could someone help me, please?
As far as I can tell, the layout of the file on the Mac vs iOS is exactly the same. Are you talking about text wrapping? You can see that on the Mac by resizing the browser.
If you want to handle the line length differently you'll need to do so by setting up scrolling.
UPDATE with more detail:
This "sort of" changes the original content :). Basically, you need to tweak both the contentSize of the webview and embed the text file in some boilerplate HTML to reflow the text rather than have the default viewport width assigned to the text document. The latter I accomplish with something like:
NSURL* url = [NSURL URLWithString:#"http://www.partisani.net/35.txt"];
UIWebView* vw = (UIWebView*)self.view;
vw.delegate = self;
NSData* Data = [NSData dataWithContentsOfURL:url];
NSString* aStr = [[NSString alloc] initWithData:Data encoding:NSASCIIStringEncoding];
NSString* responseStr = [NSString stringWithFormat:
#"<HTML>"
"<head>"
"<title>Text View</title>"
"</head>"
"<BODY>"
"<pre>"
"%#"
"/pre>"
"</BODY>"
"</HTML>",
aStr];
[vw loadHTMLString:responseStr baseURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]]];
[aStr release];
The vw.delegate = self is important, as you also need to have your controller conform to the UIWebViewDelegate protocol and implement the webViewDidFinishLoad: method. There you can set the scroll width and height of your webview as needed:
- (void) webViewDidFinishLoad:(UIWebView *)webview {
UIScrollView* sview = (UIScrollView*)[[webview subviews] objectAtIndex:0];
sview.contentSize = CGSizeMake(1000, 800);
}
This is an extremely barebones implementation--presumably you would also want logic to calculate the necessary width and height based upon the loaded text rather than use constants as shown here; you'll need some parsing logic associated with the original data for that, but this should get you started.
Setting the scalesPageToFit property to yes might fix your problem.
I'm not sure this is actually something to do with TXT or ASCII, but rather the UIWebview resizing the content.
The file you mentioned loaded into a landscape ipad screen has exactly the same layout as on Mac Safari:
You can change whether or not the UIWebView scales its content with the scalesPageToFit property.
I must include some 'rich text' instructions* preceding a form; and thought adding a UIWebView to the header of the tableview would work. The HTML is a small file in my resources folder, the CSS is in a style tag in the file; so it's all nice and self contained.
The problem is, the view transitions in; then after a small delay the contents of the webview appear. The effect is jarring, and I don't think hiding the view and fading it in when it's ready would be any more desirable.
I'm creating the view with the code below, in my viewDidLoad method.
UIWebView * wv = [[UIWebView alloc] initWithFrame:CGRectMake(0, 0, self.tableView.frame.size.width, HEADER_HEIGHT)];
[wv setBackgroundColor:[UIColor clearColor]];
[wv setOpaque:NO];
NSString *path = [[NSBundle mainBundle] pathForResource:#"HeaderMsg" ofType:#"html"];
assert(path != nil);
NSData *htmlData = [NSData dataWithContentsOfFile:path];
[wv loadData:htmlData MIMEType:#"text/html" textEncodingName:#"utf-8" baseURL:nil];
[self.tableView setTableHeaderView:wv];
[wv release];
I found 2 ugly workarounds, but I'm hoping someone has a better solution; since my workarounds make a real mess out of the code:
On previous screen when you make the
UIMYSCREENViewController, call a
[vc
preloadWebViewWithDelegate:self];
method which will make the webview
using the caller as the delegate.
The caller then retains the vc and
waits for the webview to sent it a
webviewDidFinishLoad method, at
which time it can present the view
and release the vc.
The calling view can make the webview,
wait for it to finish, then create the
new view and pass the webview into it.
At any rate, both of those "solutions" make me gag a little, so I'm hoping others have found a better way.
(*The instructions are mostly simple, styled text with some bullet points (no images or overly aggressive styling); but it takes about 14 carefully aligned UILabel views to simulate this without a webview - and is subject to the whims of the customer wanting to change the message.)
I would create another independent model or controller object to create and retain the webview, hopefully at a higher level and before (maybe during app init) displaying the view with the UI that could bring up the webview.
Consider this the same as pre-staging resources for an action game so that they don't have to be loaded during the game loop, which is a common design pattern.
Consider using this
https://github.com/Cocoanetics/DTCoreText/
I'm displayed a small amount of local content in a UIWebView, roughly 3 paragraphs of text and one 100x100px image. When the UIWebView is pushed onto to the nav controller, it takes about half a second before the content is displayed. This occurs on the device, not in the simulator. Obviously the UIWebView needs to do some work before displaying the content, but is there some way I can do this before the pushed view appears? I have not had any luck doing this myself.
Here is where I'm creating and pushing the view controller which contains the UIWebView:
ArticleViewController* viewController = [[ArticleViewController alloc] init];
viewController.article = article;
[viewController view]; //touch the view so it gets loaded
[viewController loadWebView]; //load the web view content
[self.navigationController pushViewController:viewController animated:YES];
[viewController release];
And here is the loadWebView method:
-(void)loadWebView {
NSString *path = [[NSBundle mainBundle] bundlePath];
NSURL *baseURL = [NSURL fileURLWithPath:path];
NSString* content = article.content;
NSString *html = [NSString stringWithFormat:#"\
<html><body style='background-color: transparent'><style type=""text/css"">body {font-family: helvetica; color: black; font-size: 10pt; margin: 10px 10px 10px 10px}</style>\
%#<div style='text-align:center'><a href='13443574_9709e4cf37.jpg?photographer=test' style='-webkit-tap-highlight-color:rgba(0,0,0,0);'><img src='13443574_9709e4cf37.jpg' height='160' width='230'></a></div></body></html>", content];
[self.webView loadHTMLString:html baseURL:baseURL];
}
Previously I had [viewController loadWebView] in the viewDidLoad method, but the result seems to be the same. A blank screen when the viewController is pushed, followed by the content loading half a second later.
Any ideas?
Problem Confirmed
I too see a half-second or more delay with a white screen before my UIWebView content appears. This happens only on the first use of a UIWebView during that run of the app. Successive appearances of UIWebView are nearly instantaneous. So it seems to me and other folk that the delay must be due to WebKit libraries needing to load and initialize.
Warm-up WebKit
You cannot eliminate the delay. But you can move that delay to the start of your app, to lessen the annoying effect of "blank screen" to your users. The trick is to load UIWebView with a bogus page off-screen, during your app's launch. Build a minimal HTML5 web page in a hard-coded string. I use a correct and valid HTML5 content to minimize the time taken by UIWebView/WebKit to analyze.
This technique works noticeably well on real hardware (iPhone 3GS), not just the iOS Simulator.
In my app delegates didFinishLaunchingWithOptions method, the bottom of the ARC-compliant code looks like this:
…
[self.window makeKeyAndVisible];
// Performance optimization: Warm up the UIWebView widget and its related WebKit libraries.
// We are choosing the trade-off between (A) a slight delay here during app launch to create and abandon a bogus UIWebView instance, and
// (B) a flash of white and noticeable delay as the UINavigationController slides in from the right the first UIWebView we display during the app run.
UIWebView* bogusWebView = [[UIWebView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]];
NSString* html = #"<!doctype html><html lang=en><head><meta charset=utf-8><title>blah</title></head><body><p>bogus content</p></body></html>";
[bogusWebView loadHTMLString:html baseURL:nil]; // Load the page.
bogusWebView = nil; // Not really needed, but self-documents that we intend to discard this object.
return YES;
This technique seems to reduce most but not quite all of the approximate half-second delay during the user's first appearance of UIWebView on screen. I conclude that most of the delay is due to WebKit warming up, but there must be some overhead related to graphically presenting a UIWebView on-screen. If so, we cannot eliminate that remaining overhead with off-screen work. But nevertheless, this technique eliminates most of the initial delay. So the user's first impression of my app won't be "slow".
You're going to make the user wait, the only question is: is it before or after the webview appears? If you're set on "before", then you should create the controller, load the web view, but wait to push it until the -webViewDidFinishLoad: delegate method fires. Once you receive that, you can push the view.
I have an RSS feed that gets arranged in a UITableView which lets the user select a story that loads in a UIWebView. However, I'd like to stop using the UIWebView and just use a UITextView or UILabel.
This png is what I am trying to do (just display the various text aspects of a news story):
I have tried using:
NSString *myText = [webView stringByEvaluatingJavaScriptFromString:#"document.documentElement.textContent"];
and assigning the string to a UILabel but it doesn't work from where I am implementing it in webViewDidFinishLoad (--is that not the proper place?). I get a blank textView and normal webView.
If I overlay a UITextView on top of a UIWebView on its own (that is, a webView that just loads one page), the code posted above works displays the text fine. The problem arises when I try to process the RSS feed .
I've been stuck wondering why this doesn't work as it should for a few days now. If you have a better, more efficient way of doing it then placing the code in webViewDidFinishLoad, please let me know! Does it go in my didSelectRowAtIndexPath?
Thank you very much in advance!
I think the you should first log the string returned by :
NSString *myText = [webView stringByEvaluatingJavaScriptFromString:#"document.documentElement.textContent"];
... in the case of the RSS feed to make sure that you are getting something back. It's possible the RSS page doesn't have the same javascript components and that it returns an empty string.
Once you've confirmed that, then it becomes a simple matter of getting it to display properly in the text view.
If the NSString you want to display is not empty, try to do something like this in the webViewDidFinishLoad method:
[yourUILabel performSelectorOnMainThread:#selector(setText:) withObject:[NSString stringWithFormat:#"bla bla %#", #"more bla"] waitUntilDone:YES];
The main thread of an iphone app is responsible for drawing components, that is why your label doesn't show your text.
You could also try setting setNeedsDisplay: to true
Also, the UILabel will not preserve the HTML format. It will display it as just text.
You could try the following:
NSString *htmlContent = [webView stringByEvaluatingJavaScriptFromString:#"document.documentElement.innerHTML;"];
NSString *htmlContent = [webView stringByEvaluatingJavaScriptFromString:#"document.body.innerHTML;"];
NSString *htmlContent = [webView stringByEvaluatingJavaScriptFromString:#"document.body.innerText;"];
You lose out on formatting information with the last line of code.