iPhone • 55:44
iPhone SDK makes the WebKit engine that powers Safari available to every iPhone application. Using the UIWebView class, you can load web pages directly inside your application, display styled text with HTML and CSS, and display PDF and iWork documents. Learn to leverage your existing web content inside a native iPhone application with communication between your JavaScript and Objective-C code. Find out how to make the most of UIWebView in a clean and efficient manner.
Speaker: Wil Turner
Unlisted on Apple Developer site
Downloads from Apple
Transcript
This transcript has potential transcription errors. We are working on an improved version.
My name is William Turner and I'm in Developer Technical Support and I've probably worked with some of you over email. So let's get started. UIWebView is how you can take the internet and rich content and display it directly in your app. You don't need to exit to Safari, its right there, part of your app. So in this session we're going to cover what UIWebView is, how to use it, we'll dig into the best practices for using UIWebView.
Then we'll tackle some advanced topics, some more interesting, trickier challenges, and finally we'll cap it off with some new editions in iPhone OS 3.0. So UIWebView, rich content in your app. Let's start off with text. The challenge on iPhone is that text presentation is simple. You have UILabel, UITextField, TextView, you can draw strings directly, but in all of these cases, it's a single font, a single color.
That's great for a lot of uses, but sometimes you just need to add some style. So, here's another issue. You've got documents and you want to display them in your application. Maybe it's a business app, you want to show a spreadsheet, or you'd like to show word processing document, how do you do this? You need to display a PDF, iWork, you want to view these documents but how? Another thing, there's a big Web out there, there's a lot of content, you can enhance your user's experience by tapping into that content. You know, we can always launch a URL to open it up in Safari.
But how much better would it be if we could just keep it right there in the app? Or maybe you're a web developer and you already have a really incredible web app. But you'd like to tap in to native functionality. UIKit, Address Book, Accelerometer, MapKit, you name it. Well, you don't want to start over, you just want to enhance what you've got. So the solution to all of these challenges is UIWebView. It's a subclass of UIView, which means you can embed it directly in your View hierarchy, just like you would any other view.
And it renders a variety of content types that we'll talk about, it provides basic browsing function, forward, back, reload, and it's powered by WebKit which is a first class standard compliant rendering engine for web content. WebKit supports the content types, of course HTML, and that's HTML 5.0, with CSS, JavaScript, and also renders PDF directly.
You want styled text in your app? Just use HTML. You can't get much easier than that. So UIWebView also adds documents. WebView converts these documents to HTML and then WebKit renders them and then that result is displayed in UIWebView. The supported types of documents include iWork 06 and newer, Microsoft Office 97 and newer, and now in 3.0 we've added rich text and rich text directories.
So UIWebView is the solution to all these problems. Use it when you want to display style content, show document previews, embed web resources in your application, and you want to extend web apps with native functionality. So that's what UIWebView is. Now let's talk about how to use it.
So I've got a couple design patterns to talk about but I won't get too academic with you. We've got UIWebView which is the main class, and then you've got UIWebViewDelegate which is a protocol that your, one of your controllers in your application, probably a view controller will adopt this protocol and that's a way for the WebView to notify the controller when a load is happening, when it's finished, or if there's an error, and allow you to handle that. And then the smaller, the bit players in this little cast are the URL's, your HTML content, and JavaScript of course.
So how do they interact? Well it's probably going to start with your controller, it's going to request that the WebView load a URL or just some HTML data directly, and anytime the WebView is asked to load a resource, it goes right back to the delegate and says do you want me to load this? Because sometimes you might not want it to load that or you may want to take some modifying action with the load.
Then it will start loading the request and this could take some time because it's often a network activity. And when it finishes or if there's a problem, it'll cap it off by letting the delegate know what's happened. The life cycle of a WebView. So there's a little caveat I want to touch on here, mostly you've probably, it's pretty obvious, you start off by creating the WebView and then you're going to set its delegate, you're going to load the content, work with the content, do whatever you need to do, and then at some point you're done with it and you want to release it.
We just have to insert one really important step here. Before you release your WebView, set the delegate to nil. The reason for this we'll dig into more depth later but it has to do with multi threading. WebView does a lot of stuff in the background for you so it doesn't block your apps, your application's execution.
However, if you release the WebView and it's delegated as a nil, some of those background activities may come back and find themselves talking to an invalid pointer and we don't want that. So another issue of working with WebView is how do you communicate from the Objective-C code to your native code? Or back and forth? So you'd like to execute some JavaScript directly in your web content, this turns out to be extremely easy.
The WebView has a method called stringByEvaluatingJavaScriptFromString. So perhaps you got into a JavaScript function, it's called Insert. So an Objective-C would just put together a string with the text insert, and you pass it to this method. Then the Insert function in the JavaScript and the content will get called, and if it returns anything, that will get passed back through this method as well. Going the other direction from JavaScript to Objective-C, this turns out to be a little trickier because there's not really an explicit API.
But when would you want to do something like this? Well the great example is if your web content has AJAX running, and you get data back asynchronously and you'd like to update something in your native UI, maybe you want to update a status, or display a list of items in a native table view, so when that JavaScript handler, when that AJAX handler gets called, you need some way to pass that information up to the Objective-C layer. So like I said, there's no explicit API for this.
But you can tap into that delegate method where again, when the WebView asks the controller, its delegate, if it should load a request, we're going to sort of overload this with some additional functionality. Basically what we do is we create a URL that's with, got a dummy host. That's, you know, not a real URL and we're going to use the path to specify the command we want to execute an Objective-C. So this is what it might look like. Obviously, that's not a real URL. So we might use window.location to trigger a load.
So in our delegate we test the URL. The first thing we do is we grab the host out of it and we just do string compare and we see if it matches my JavaScript. Then we know that we're not really loading real requests here. We're doing something special.
So then, we grab the command, which we're using the path for, and we might have multiple commands that we test the command to see which one it is and if it matches, then we do something. And I'll show you a demonstration of this a little bit later on. So putting UIWebView to work.
We've got WebView and WebViewDelegate, the message flow between the delegate and the WebView, that really important life cycle detail of setting the delegate to nil before releasing, then we talked about how we communicate back and forth between the native content and the web content. Both from Objective-C into JavaScript and then back from JavaScript into Objective-C. So now, let's build a little hybrid application that combines some web content with some Objective-C functionality.
Here's what it looks like, very simple, it's got the embedded HTML content which will have a little bit of CSS and a little bit of JavaScript but any of you web developers out there will laugh when you see how simple it is. And we've got some native components, we've got of course the WebView itself, and we've got a UI toolbar at the bottom.
And in that toolbar, we have a text field. And then we had hooked up some communication between the WebView and its native code. And when we hit that HTML button, it's going to show an alert, and that, you could actually do that just with plain JavaScript, but I thought an alert was the simplest way to show you how this works.
And then when we enter text in the text field, we're going to use JavaScript to inject that into the DOM of the web content. Again, nothing too complicated but interesting enough. So let's switch over, alright, so I'm going to go to Xcode, I'm going to create a new view controller based application.
I'm going to call it hybrid app, and save it to the desktop. Now the first thing I want to do is go in and the template has provided me with a view controller, but to speed things up I went ahead and wrote it earlier. So we're just going to throw away the one they gave us and we're going to drag in this one that I wrote. Then I'm going to copy it in and then I'm going to grab this simple little webpage I wrote and make it a resource in the project. So let's have a quick look at that webpage. Like I said, very simple, the whole thing fits on the screen.
Just have a little bit of CSS at the top, then we have our two JavaScript functions. Now the one, the insert, we're going to call from Objective-C, and you see it does some very basic DOM manipulation and then down here we have this function called invoke Objective-C and that does windowed out location to load that dummy URL.
The HTML itself, also very simple, we just have the button and the little prompt here and then an empty DIV container, which is where our injected text will go. So now, I'm going to look at the header file for the view controller. This tells us a lot about the UI we're going to build. We see that it's a WebView delegate and it's also a text field delegate so again, TextView is like web views. Use a delegate mechanism to communicate with a controller.
Now we have three native elements here, the WebView, the text field, and the toolbar and we have IB outlets for each of those. We're going to go kind of slow with interface builder in case you're not too familiar with it. So here, we have our Nib file and this is our view controller and this is the basic view.
The first thing I'm going to do is I'm going to find a toolbar and I'm going to drag that in to the bottom of my view here. Now toolbars can have multiple items and they position these from left to right. Now the default item is not interesting to me, I want just a text field. And I want to place it down here, but I'd like this text field to be centered.
So to do that I need to use some flexible space items. And that's just a positioning tool for toolbars. I put this in here, and boom it pushes it all the way over. So I'll need one more, just to balance it out so it's centered. Now the arrows you see on there won't actually display at runtime that's just a note for you so you know what's going on. I can stretch this out and that's pretty good. Finally, I need to add my WebView. So drag that down, it sizes itself to fit the space, and the next step is to connect all my outlets and my delegates.
So from my file's owner, I have a text field and a toolbar and the WebView. Going the other way, select the WebView and now I've got the delegate, which goes back to my file's owner, and let's get the text field and connect it. And that's it. Our UI's done. Let's go back to Xcode.
I'm going to build and run it so we can see what this looks like. Alright, so we've got our web content here, scrolling forth, let's try out this HTML button. And there we go, pretty cool. Alright, let's try the text, just type something simple in here. And there it goes. So from Objective-C we're calling JavaScript, which inserts arbitrary text that we want into the DOM of our content. One more time, and you see it just scrolls on down. So click here, we distance the keyboard.
Let's go back and look at the code, see how this works. So we'll switch to this file. So it's not too big a file but to make it simpler I'm just going to fold up all these methods, okay. So the first block at the top here, this is all of our kind of life cycle manipulation.
I'm not going to worry about it too much but I'm going to open these up so you can see when the view unloads which can happen in response to a low memory notification, I'm setting my outlets to nil but before I set the WebView to nil I'm setting the delegate to nil. Likewise, in the dealloc I do pretty much exactly the same thing. So now let's go up to viewDidLoad so, this is when things first get started. I've dragged my webpage in as a resource so that means it's bundled with my application.
So at runtime I want to find that bundle, that file, and load it up as a string and pass that string to my WebView. So the first thing I do is I get its path and then I weave the file in as a string. And then I tell the WebView to load that string. There's no external URL upon which this depends.
So the base URL is nil. Now we get into the interesting stuff. This is what happens when we try to load a new page. We get this delegate method called webview:shouldStartLoadWithRequest:navigationType. The navigation type might be the user click to link, or the header Back button or Forward button. Now in this case the navigation type would be other because JavaScript triggered it. So just like this on the slides, we get the host, we compare it, if it's a match we go in, get the command.
If it's a match, here we put together our alert view, just very simple, our title, message, and the Cancel button. Now after this, we return No because they should load request, that's your chance to say yes you may or no, you may not. So we don't want to actually load anything here, we're doing something different.
But in all other cases, we would return yes. So now, let's look at what happens when we enter some text. This TextFieldDelegate method is called when you hit the Return key on the iPhone keyboard. Again, we're taking the string from that text field and we're wrapping it into this insert function. So it's being passed as a parameter. And you know that we're single quoting it here.
Then we pass that to the WebView and next we just clear out the text field so it's empty and ready for the next time. And we'd return yes because we do want to allow it to return. Some of you may be thinking wow you just took arbitrary text and just passed it through and that's not very safe.
Well you're right, if this were a real app, I would want to take this text and check and see if it's got any quotes or other extra characters that could cause some problems and filter those out. But I wanted to keep things simple for this. So let's go back and run the app one more time.
And see some of this in action again. So there's the button, and then down here we enter our text, and it just goes right in. And there we go. Very cool. So you can see it's very simple to combine native functionality with web content in an app. So now, we've talked about what UIWebView is and how to use it, let's get into best practices.
And best practices sometimes kind of sounds like someone's lecturing you about what to do, what not to do, but what I really think it is, is it's knowing about what you're using so you can get the most out of it. With anything, it's always using the right tool for the right job.
And with UIWebView, there's some jobs for which it's absolutely the right tool and other jobs for which it's not the best tool. So let's talk about what's in a web browser and what's in UIWebView. On Mac OS X we provide the full WebKit framework, completely exposed to developers, so they can build their own web browsers. Well let's see what's in that. Wow 125 different classes, and some of these are DOM manipulation classes, script handling classes, web content classes, frames, etcetera. Twenty-seven protocols for communicating with delegates and other objects.
Over one thousand API's. Now on the iPhone we have UIWebView. one class. WebViewDelegate, one protocol. A total of twenty API's. So I'm going to go out on a limb and say you don't have everything you need to build a full web browser with UIWebView. And there's a reason for that. The iPhone has an incredible web browser already. So with UIWebView it's about presenting your content.
It's a lightweight presentation layer over WebKit. It's optimized for presenting style content, your HTML, your JavaScript, etcetera, and it works best when you know the content. So if you've got an existing website or a website that's relevant to your application, it's a known domain, that's great, that's perfect with UIWebView.
But general browsing, you can get into a little trouble there. Just the user starts clicking links and soon they're all over the web, they're ten miles from where they started with your app. It doesn't really make sense. So that delegate method shouldStartLoadWithRequest is your chance to exercise some control over what's shown in your app.
And you might say well I'll start off with a domain that's appropriate, my domain, and any request that exits my domain, instead of showing it in my app, maybe I'll just launch it in Safari and then the user can have a real browsing experience. Or maybe I'll prompt them to see if they'd like to exit and go to Safari. But just allowing yourself to spider all over the web isn't going to work very well with UIWebView.
There's also memory consumption to talk about. So UIWebView does require significant resources. Think about the Internet for a moment and what's the largest webpage out there All the images on it, the JavaScripts that might take several minutes to run, megabytes potentially. So with the WebView its memory footprint is really predicated by the size of the content you load on it. There's no official limit on how big a webpage can be.
So that can be a problem with WebViews. And finally data may be cached internally by WebView or by some internal frameworks on which it depends. And you don't have an explicit mechanism for releasing that cache in the case of a low memory situation. So you know, content is a consideration. So what about multiple web views? Well I'm not going to give you a hard and fast rule on this because there isn't one. But just keep in mind, more content equals more memory.
Be conservative in your use of web views. Let's consider JavaScript for a moment. So JavaScript is really fantastic and WebKit's a really fast processing engine for it. But on the iPhone you really want to watch out and make sure your scripts are fast. They really, they have to finish within ten10 seconds and if you asked me personally I wouldn't want a script that took that long on a web page I was using.
But if you've got longer scripts, break them down into chunks and then execute each chunk serially. There's also a little bit to think about with power consumption. Network is one of the major power consumers on the device, the others being the screen and disc I/O. So you want to avoid rapid polling with AJAX where you're just bouncing off the server every second or every thirty seconds and try to batch lots of small data transfers into larger chunks.
Threads. So I mentioned earlier that you want to set the delegate to nil before you really selectView and that this had to do with threading. Well first of all before I get into that, you need to know that you need to know that UIWebView should only ever be accessed on the main thread of your application. Of course that's true for all UIViews. Be extremely careful if you are threading in your app that you're not handling any views off of the main thread.
Now UIWebView uses background threads to download its content and to render it. So the benefit of this is your application won't block. You can create a web view, tell it to go off and load something and your app will keep running, keep being responsive while the web view is doing this work in the background. And it'll just come up and be presented to the user when it's ready.
But this is why we have to set the delegate to nil. Those background requests if the web view gets released and delegate goes away and they come back, we could have a crash. We don't want that. So just, it's very easy, just set the delegate to nil before releasing. A lot of developers ask me about how to get touch events in a web view.
Not to be manipulated by their content but to handle something extra in the native logic. And they will, maybe I can subclass UIWebView and override the UI responder events for handling events. Well the problem with that is that UIWebView's internal view hierarchy is private, it's complex and it's not safe for developers to go and tamper with.
And event handling goes deep into this view hierarchy. So instead what we suggest is not subclassing UIWebView, there's a better way. You can do it at the window level. You can subclass the window because all events will go through the window before they get to any view. So you override sendEvent. And in sendEvent the very first thing you should do is call super sendEvent because I use the word observe.
You should only observe the events, you should not tamper with them or try to modify them in any way. So if you see that an event is going on that you're interested in, it's perfectly legitimate to add some extra functionality on your part. But you don't want to interfere with the existing functionality. So always call super.
Now it goes without saying that if the changes you want to do are at the content level, then you should really just stick with the JavaScript event handlers. They're very powerful and will let you do just about anything you need to do. So let's take our existing demo and add some touch event handling to it. So where we last left our web app we've got the ability to enter some text. Now the first thing I've noticed is I can't scroll down because as soon as I do, the keyboard goes away. That's a bit frustrating.
So the first thing I'm going to do is I've prepared some little snippets of code and I'm going to add these in to the demo. Now to keep the keyboard from going away when I don't want it to go away, the first thing I'm going to do is add a little Boolean that I can use to track the state. And this I'm calling textfieldShouldResignFirstResponder. Resigning first responder is how the keyboard goes away. And in this case, there are sometimes when I don't want to resign first responder.
So what I'm going to do is add implementation of another text delegate method and this one's called textfieldShouldEndEditing. And that's what happens when first responder, when it tries to resign first responder. But I'll use my Boolean and say well if this isn't set, I'm not going to resign first responder. But if it is set, then I'll go ahead and resign but reset that flag back to nothing. So let's run the app again.
And enter some text, and some more, great now I can scroll. I'd like to just tap and have the keyboard go away now. But I can't get rid of the keyboard. So I'm going to have to add some more code. Now I talked about subclassing UIWindows. So I have the subclass of UIWindow written that I'm just going to add right in to the project.
So let's look at this subclass. Well the first thing I see is a protocol at the top, it's a tap observer protocol that I've created. And this is a way for the window to communicate back with some controller object that is interested in getting tap events. So then the window itself, I have the view that I'm observing, so I'm only carrying about tap events in one view. And I have some controller, I don't really know what type it is, but I know that it conforms to this protocol.
And then I have some properties to manage those variables. So here this is very simple. We have send event which is the workhorse and the first thing I do is I call super, and then we go down and I want to do as much filtering of the event handling as possible because it's possible for events to flow through the system very rapidly if the user's doing a touch that moves across the screen, that will result in a lot of calls.
So I want to minimize the work I do. The first thing I do to eliminate events I'm not interested in is I see if I don't have a view that I'm observing or a controller that is observing, I just stop right there. The next thing I do is I get the touches in the event.
I'm only interested in events with a single touch. So if the count of touches is not 1, I stop right there. Moving forward I get the touch out of the event and I check its phase. So a touch can have a begin, a moved or an end phase.
I'm only interested in touches that end. So if we're still, have gotten this far, I have a slightly more expensive call which is to see if this touch occurred in the view that I care about. So I actually use isDescendentOfView because it could be in any sub view within that view.
If it's not, then I return. The last part is a little trickier. So what happens when you have a single tap? You get a tap count of 1. What happens if you have a double tap? We actually get two separate event messages. The first one has a tap count of 1, the second has a tap count of 2.
So when you get the tap count of 1 you have no way of knowing if a tap count of 2 is about to follow it up. So what we do is I'm just going to delay execution of my response to the tap by half a second. This is an NSObject method called performSelectorWithObjectAfterDelay. And then I wait.
And in the next half second, if the double tap comes in, I can use cancelPreviousPerformRequestWithTarget to cancel that request and I will ignore the double tap. If a double tap never comes then this forward tap method, which is just a little helper method in my own class, will take place and my controller will be informed that the user tapped my view. So it's pretty straightforward.
The next thing we need to do is go back to our controller and add some code so it actually uses this new window. The first thing I have to do is in the header of the controller, import my window class, and then I want to add conformance to that protocol. So now my controller conforms to the tap observer protocol in addition to these others. That takes care of the header file. Now in the implementation I have to actually start observing so I'm going to do that and view will appear. Or view did appear.
So I've written this code that just drops in here. So what this does is I get the window from my view and I cast it to a tap observing window. Then I set its view to observe to be my web view and I set the controller that observes to be myself. Then to be good about this, when the view disappears, I don't want to continue observing. So I'm going to help the window be more efficient by stop the observing. So that just sets these properties back to nil.
And the biggie of course is responding to the tap. So what are we going to do with that? Well in responding to the tap, I just have to do two very simple things, I set my personal textfieldShouldResign flag to yes and then I ask the text field to resign.
So when there's a tap I want the keyboard to drop out. The only thing that's left is going into my main Nib and changing my window to be not just a UI window, but a tap observing window. Why don't we go back to the app, and run it.
So let's enter some text and we can scroll, great, tap, and the keyboard drops out of sight. Let's go back in and let's try double tap, keyboard stays. Single tap keyboard disappears. So it's that easy. Going back to the code just one last look at the event handling here, we start with super and then we do some filtering, so we minimize the work we do.
And then we, then what really matters is handling the exact kind of event you're interested in. I'm caring about phase ended single taps but it could be any event, it could be a multi touch event. It's really up to you and what you want to do in your application.
So there's some best practices for UIWebView then we demoed how to handle touch events, which a lot of developers have asked us how to do that. Let's talk about some advanced topics. So in this, I'm going to talk about NSURLConnection, which is a foundation level technology, and then I'm going to talk about handling HTTP authentication in UIWebView.
Next we'll explore links that open in new windows, how to save and restore state with UIWebView and then we'll finish up with offscreen rendering which is basically taking the content of a web view and creating an image from it. Let's start with NSURLConnection. So this is a lower level API but it's very simple to use and it's how you can download content directly which turns out to be a very powerful tool.
So once again, it works with a delegate API and, the, create a connection, you give it a URL request just like you would give your web view and it can call back to your delegate as data is received or if there's an error. So it has two modes of operation, it has a synchronous mode and an asynchronous mode. Don't use the synchronous mode, it blocks.
So if you do a synchronous connection to retrieve some data from a server, your app is going to sit right there and spin until it has all that data. You don't want to do that on the main thread, and you might think well I'll just create a separate thread and do that on my separate thread. Well that's really not the best approach either because that creates a threading complexity in your application. Instead if you use the asynchronous mode, our foundation in the background will do the threading work for you and you don't have to worry about that complexity.
So it's fundamental for a lot of advanced use cases. Let's look at one of those right now. HTTP authentication. So UIWebView, I think I mentioned it's not a full browser replacement. One of the things it doesn't do for you is automatically provide a UI for authenticating to some site that's protected with HTTPAuth. However there's a simple way and a tricky way. The simple way for you to handle this is just to embed that authorization information directly in the URL request. If you do that, you should use HTTPS so that, that information is secure.
Now if you do want to present a UI for authentication, here's where NSURLConnection comes in. You create a separate connection for the URL and in the connection delegate you'll get a didReceiveAuthentication challenge And then you can provide your custom UI to the user, they can enter the username, their password, and what you're going to do is you're going to store those credentials for the persistence of the session which is the lifetime of the app. Then you go back to your web view and you load that URL request and it will use the cached credentials.
Now you might say well but now I'm downloading all that content twice. Well you could do that, but you don't have to. With NSURLConnection you can path an NSMutableURL request and in the mutable request you can set the HTTP method. So usually you probably use get or post, but if you just use the header method, you'll only get the header back from the server and you won't download any of the body content.
So links that open in new windows, there's two ways this works. Its target="_blank" and JavaScripts window.open(). Both of these are ignored by UIWebView because if you think about it the web view is just a view in a window, it has no knowledge of how you want it to suddenly pop open a new window.
So it just silently ignores these requests. If you're dealing with content, well, the first thing is if the content is something you control, just don't use those links. Just use standard links that will open in the same view. Now if you're dealing with content you don't control directly and you know that this content has links that open in new windows, here again, NSURLConnection.
You download the content into memory, and you can parse through and replace those instances and then load that data from memory directly to the web view. And the links you might want to set them up so they will just open to right within that or you can do whatever you want.
You can, just as we did with the JavaScript to use the webviewShouldLoadRequest, you can modify those links so that your web view delegate has a way of knowing that you want them to open in a different window or a different view or have some other functionality and then deal with them, as you want.
Saving and restoring states. So with the iPhone we emphasize that the user can exit your app at any time, a phone call could come in, they could hit the lock, whatever. So you want to be ready to shut down instantly and the next time the user runs the app, you want to be able to return right to the same place.
So how do you do this with a web view? Well once again this is one of those areas where if you control the content, this turns out to be a very easy problem and if you don't control the content it turns out to be a somewhat more difficult problem.
If you control the content then what you want to do is keep your JavaScript and your CSS in line and you want to minimize external resources. That way you can do the same thing with NSURLConnection, you can download it separately from memory, pass it to the web view and then if your app terminates you just write what's currently in memory out to a file. And the next time you launch you just have to see if that file's there.
And then load the content from that file and push it right to the web view. And this works great again if the JavaScript and CSS is in line. If it's not, then you can end up with something that doesn't look quite right. The reason for this is NSURLConnection downloads a single URL.
WebView will download the URL and then each external resource in it, it uses additional connections to download those. So if you don't control the content and you're going to try to do this robustly you would actually have to download the main URL and then look through and find all the other resources and download those as well so you have them in memory.
And that can be kind of tricky. Finally offscreen rendering. So you've got a webpage and you'd like to show a thumbnail image or a full size image, you want to capture it, render it somehow to an image. Well this turns out to be fairly straightforward. You just load your content in a web view and then you use some UIGraphics methods to render those contents.
You would access the web view's core animation layer and render that after it's been drawn. And it's not too hard. There are two challenges you have to deal with. The first is that Webview won't render unless it's in a window. And if you want to do this offscreen, it won't be in a window unless you create a new window and place it offscreen and that's what we'll do. The second thing is that drawing is asynchronous.
So as I mentioned UIWebView uses threads both to download content and actually also to its drawing. So when your web view delegate gets the loaded finished message, that just means that we finished grabbing all the content off the web. It doesn't mean that we've actually finished drawing all the content. So you'll have to basically build in a little delay just like we did with the event handling.
So now we're going to demo an application that does some offscreen rendering. I'm going to create a new project, again I'll use the view based template, call it offscreen renderer. And again, the first thing I'm going to do is go in and remove the view controller that was provided with the template and replace it with one I've written already.
Okay. So let's look at the other file because that's going to tell us a lot about the UI that we're going to build. This is very similar to the other app as you see. We've got the web view delegate and the text view delegate. We have a different group of native elements here to work with.
We have an activity indicator, an image view, a label, our offscreen window, and a web view. We also have this flag that's a little state variable saying whether or not we're in the process of rendering a page. Now we have properties to manage each of these variables and you'll notice that all of them are IB outlets except for the web view. The web view as it turns out, we're going to create in code instead of in interface builder.
So let's switch and open up our Nib and we'll close these other Nibs. And then we start off with this NPView and this time I'm going to build my UI just a little differently. Instead of having a toolbar at the bottom, I'm going to put a navigation bar at the top and put my text field in there.
So find the navigation bar, right here, and find the text field, and we drop that in the middle, and you'll notice that this is positioned in the center instead of over to the side. Navigation bars have, instead of just an arbitrary set of items positioned left to right, it has a center item and a right item and a left item.
And so we're using the center item and we'll give them a little stretch to fill, and the next thing we want to do is put a label at the bottom of our display and this will just be a little status message, just a, I'm going to center that label and clear out that placeholder text.
Next thing is an image view that will show the final rendered contents of our webpage. I'll drag that down here, there we go. And while we don't have our content, I want to show an activity indicator over top the image view. And then I'm going to set that to hide when stopped. So we won't see it except when it's actually working. The last thing I need is my offscreen window. So I'll just drag that directly into the interface builder document. And let's make our connections.
We have the files under, it's got its activity indicator, and has the image view, and label, the offscreen window and then finally we just have to set our text fields delegate back to the file's owner. And with that we can, we're ready to run. So let's try Apple.com. So there we go, an image representing web content. You can see this is not web content, it's a real image.
Let's try another one, let's try developer. And it looks like I must have made a typo. Let's try, its offline? Oh, even better. Let's try store that'd better be online. There we go, okay, so you can see it's just that easy. We grab our web content and we draw it to an image and present that image on the screen. So let's look at the code and see how that's working.
Once again, I'm going to go ahead and fold everything up, make this a little easier to look at, you can see again this is not too big a project. The lifecycle methods are exactly as they were before, I'm setting the delegate to nil before I release the web view. So let's start with when the text field begins editing.
Just because I'm a little lazy, I start off the first thing I do is pop in the HTTP for me, save a little typing just a convenience. Now when we hit return, well we've got a little more going on here. Start up the activity indicator and it becomes visible when you tell it to start animating. Then I set the text fields text into my label. So we can see what URL we're loading.
The next thing I do is I create a URL object from that. Then I create my web view and I set its size to be the same size as the image view because basically whatever image you capture from the web view is going to be the same size as the web view. And you can also resize it later but that's extra work we don't need to do.
I set the web view delegate to self and I scale the pages to fit. So depending on how the page is styled it may or may not size itself to fit. But if you use this property it will. Then to my offscreen window, I add the web view as a sub view.
And I tell it to load the request and then while that's happening, again, that happens asynchronously so now web view's gone off in the background to do the work. I clear out the text field, I tell it to resign first responder, so the keyboard goes away, and I clear out any previous image in the image view.
And finally I set this flag renderingAWebpage equal to yes. The reason for this is that when you go and load a page that's got certain kinds of JavaScript in it, you may get the load request more than once. And you don't want to handle it more than once in this case where we're rendering an image. We just want to render the image one time. Believe me it's enough work rendering it once.
So now we wait for the web view to finish loading. Now remember when this method calls, this just means the web view is finished downloading the content. It doesn't mean it's actually finished drawing. So here we use our flag to make sure we only do this once, and then we're going to delay our rendering by a second. That will give a chance for all the drawing to take care of and make sure that, because if you draw too soon you might end up with a white image instead of the actual content you want.
The next thing is what happens in the rendering? So this is some UIGraphics work, you'll get a lot more information about that in some of the graphics sessions, but we begin what we call an image context and we make it the same size as the image view. Then we take the web view's layer, it's a core animation layer, and we render it into the current graphics context.
The current graphics context is this one that we just created. The next step is that we set the image view's image from the image that we retrieved from the current context. Then we finish that context so we've cleaned that up, we're done with it and then we go ahead and remove our web view from its super view, set the delegate to nil, and then release the web view itself and last of all we tell the activity indicator to stop animating which will result in a disappearing from the screen.
Let's have a look at that one more time. And there we go. So it's just that easy, rendering content from a webpage to an image. So those are the advanced topics and the sort of unsung hero in almost all of the advanced topics is NSURLConnection and its best use in its asynchronous mode. So now we've talked about web view, and how to use it, the best practices, and advanced topics let's look at some of the cool new features we've added in 3.0 now.
So CopyPaste. The good news is, you get CopyPaste absolutely for free, it's built into UIWebView, you don't have to do a single thing. You get all of the gestures, all of the functionality, you can copy from a web view into native elements, copy from native elements and paste into web view's text area, or input on a text, on a webpage back and forth. It's very easy, you can select images, select text, the works. Sometimes you may want to disable selection. If you've got some JavaScript on your page, it adds behavior to something with certain events.
The selection may not play well with that when that UI pops up. So the easiest way to deal with this is to use the cascading style sheet selector WebKit user select and set that to none. But don't do that for the whole document, just do it on a per element basis for the ones where you don't want, you explicitly don't want to tamper with the built in, with the functionality you're providing. Because otherwise you want the user to be able to basically have the same experience they have with all other content.
They can copy it whenever they want, paste it whenever they want. So the other big new thing are the data detectors. These automatically convert data in a page that is formatted a certain way like a telephone number or a URL, into clickable URLs. The telephone number would make a phone call, the clickable URL, the link would go to another page etcetera.
And there's a property on UIWebView called data detector types and this is a bit mask of the four types we currently provide which are phone numbers, a type for links of course, you can specify none, or you can specify all and there's a lot of benefit to adding all because this means that any data detector types we add in the future, your app will automatically support them without needing to recompile.
Use this instead of the detectsPhoneNumbers property. That was what we had in 2.0 and that's deprecated now, so when you're updating your apps to resubmit them to the store with new features, take a moment and replace the detectsPhoneNubmers with the data detector types. So that's what's new in iPhone 03S and that concludes this session.
We've got some additional information, you can talk to Vicki Murley, she is our Safari Technologies Evangelist and I encourage you to look at the documentation. There's a class references and there's a conceptual document called the URL loading system. It's extremely useful in understanding how to work with not just URLs but requests, credential authentication, and URL connection itself. You can find all that documentation on the web at developer.apple.com and it should also be something you can access from Xcode. So you can download the current doc sets and access that information right on your computer.