Let’s review what you’ve accomplished so far. You have:
n created a text field object where the user can type in an URL
n created a web view object that will display the web page at that URL
n added two outlets (properties) to your SUViewController class
n connected the text field and web view object to those properties
n wrote a -loadLocation: method that takes the URL in the text view and loads it in the web view
What’s missing? The question is “how does the -loadLocation: method get invoked?” That’s a really important question and, at the moment, the answer is “never.” The next, and final, step is to connect the -loadLocation: method tosomething so it runs and loads the web page.
Start by declaring the -loadLocation: method in SUViewController’s interface. Add the following line, just before the @end statement, to your SUViewController.h file:
- (IBAction)loadLocation:(id)sender;
When you’re done, your files should look like those in Figure 3-14. This declaration tells the rest of the world—well, the other objects in your app—that SUViewController has a method that will load a web page. The IBAction keyword tells Interface Builder that this is a method that can be connected to an object, just as the IBOutlet keyword told Interface Builder that the property was a connectable outlet. A method that can be connected to objects(like buttons and text fields) in your interface is called an action.
Figure 3-14. Finished -loadLocation: action
Click on the Main_iPhone.storyboard file again. Select the text field object and switch to the connections inspector. Scroll down until you find Did End On Exit in the Sent Events section. Drag the connection circle to the ViewController object and release the mouse, as shown in Figure 3-15. A pop-up menu will ask you what action you want this event connected to; choose -loadLocation: (which is currently the only action).
Figure 3-15. Setting the Did End On Exit action connection
You also want the web page loaded when the user taps the refresh button, so connect the refresh button to the same action. The refresh button is simpler than the text field, and only sends one kind of event (“I was tapped”).Use an Interface Builder shortcut to connect it. Hold down the control
key, click on the refresh button, and drag the connection to the View Controller object. Release the mouse button and select the -loadLocation: action, as shown in Figure 3-16.
Figure 3-16. Setting the action for the refresh button
Testing the Web Browser
Are you excited? You should be. You just wrote a web browser app for iOS! Make sure the build destination is set to an iPhone Simulator (see Figure 3-17) and click on the Run button.
Figure 3-17. Setting iPhone Simulator destination
Your app will build and launch in the iPhone simulator, as shown on the left in
Figure 3-18. Tap the text field and an URL-optimized keyboard appears. Tap out an URL (I’m using www.apple.com for this example), and tap the GobuttonThe keyboard retracts and Apple’s home page appears in the web view. That’s pretty darn nifty.
Figure 3-18. Testing Your Web Browser
So how does it work? The text field object fires a variety of events, depending on what’s happening to it. You connected the Did End On Exit event to your -loadLocation: action. This event is
sent when the user “ends” editing, by tapping the action button in the keyboard (Go). When you
ran the app and tapped Go, the text field triggered its Did End On Exit event, which sent your SUViewController object a -loadLocation: message. Your method got the URL the user typed in and told the web view to load it.Voila! The web page appears.
Debugging the Web View
What you’ve developed so far is pretty impressive. Go ahead, try any web page, I’ll wait. There are only two things about it that bother me. First, when you tap a link in the page the URL in the text field doesn’t change. Secondly, the web pages are crazy big.
The second problem is easy to fix. Quit the simulator, or switch back to Xcode and click the Stop button in the toolbar. Select the web view object in Interface Builder and switch to the attributes inspector, as shown in Figure3-19. Find and check the Scale Page to Fit option. Now, when the web view loads a page, it will zoom the page so you can see the whole thing.
Figure 3-19. Setting Scale Page to Fit property
The first problem is a little trickier to solve, and requires some more code. We’ll address that one as you add the rest of the functionality to your app.
Adding URL Shortening
You now have an app that lets you enter an URL and browse that URL in a web browser. The next step, and the whole purpose of this app, is to convert the long URL of that page into a short one.
To accomplish that, you’ll create and layout new visual objects in Interface Builder, create outlets and actions in your controller class, and connect those outlets and actions to the visual object, just as you did in the first part ofthis chapter. If you haven’t guessed by now, this is the fundamental app development workflow: design an interface, write code, and connect the two.
Start by fleshing out the rest of the interface. Edit Main_iPhone.storyboard, select the web view object, grab its bottom resizing handle, and drag it up to make room for some new view objects at the bottom of the screen, asshown in Figure 3-20. Select the vertical constraint beneath the view (also shown in Figure 3-17) and delete it. You no longer want the bottom edge of the web view to be at the bottom edge of the superview; you now want it tosnuggle up to the toolbar view, which you’ll add in a moment.
Figure 3-20. Making room for new views
In the library, find the Toolbar object (not a Navigation Bar object, they look similar) and drag it into the view, as shown in Figure 3-21. Position it so it fits snugly at the bottom of the view.
Figure 3-21. Adding a toolbar
Find the Bar Button Item in the library and add toolbar button objects to the toolbar, as shown in Figure 3-22, until you have three buttons.
Figure 3-22. Adding additional button objects to the toolbar
You’re going to customize the look of the three buttons to prepare them for their roles in your app. The left button will become the “shorten URL” action, the middle one will be used to display the shortened URL, and the rightone will become the “copy short URL to clipboard” action. Switch to the attributes inspector and make these changes:
n Select leftmost button
n change identifier to Play
n uncheck Enabled
n Select middle button
n set style to Plain
n change title to “Tap arrow to shorten”
n change tint to Black Color
n Select the rightmost button
n change title to “Copy”
n uncheck Enabled
Now select and resize the web view, so it touches the new toolbar. Finish the layout by choosing
Add Missing Constraints in View Controller from the Resolve Auto Layout Issues button.
The final layout should look like Figure 3-23.
Figure 3-23. Finished interface
Just like before, you’ll need to add three outlets to the SUViewController class so your object has access to these three buttons. Select the SUViewController.h file in the project navigator, and add these three declarations:
@property (weak,nonatomic) IBOutlet UIBarButtonItem *shortenButton;
@property (weak,nonatomic) IBOutlet UIBarButtonItem *shortLabel;
@property (weak,nonatomic) IBOutlet UIBarButtonItem *clipboardButton;
Select the Main_iPhone.storyboard Interface Builder file, select the View Controller object, and switch to the connections inspector. The three new outlets will appear in the inspector. Connect the shortenButton outlet to theleft button, the shortLabel outlet to the middle button, and the clipboardButton to the right button, as shown in Figure 3-24.
Figure 3-24. Connecting outlets to toolbar buttons
Designing the URL Shortening Code
With your interface finished, it’s time to roll up your sleeves and write the code that will make this work. Here’s how you want your app to behave:
n The user enters an URL into the text field and taps Go. The web view loads the web page at that URL and displays it.
n When the page is successfully loaded, two things happen:
n The URL field is updated to reflect the actual URL loaded.
n The “shorten URL” button is enabled, allowing the user to tap on it.
n When the user taps the “shorten URL” button, a request is sent to the URL shortening service.
n When the URL shortening service sends its response, two things happen:
n The shortened URL is displayed in the toolbar.
n The “copy to clipboard” button is enabled, allowing the user to tap on it.
n When the user taps on the “copy to clipboard” button, the short URL is copied to the iOS clipboard.
You can already see how most of this is going to work. The “shorten URL” and “copy to clipboard” button objects will be connected to actions that perform those functions. The outlets you just created will allow your code toalter their state, such as enabling the buttons when they’re ready.
The pieces in between these steps are a little more mysterious. The “When the page is successfully loaded” makes sense, but how does your app learn when the web page has loaded, or if it was successful? The same it truewith the “when the URL shortening service sends its response.” When does that happen? The answer to these questions is found in multitasking and delegates.
“Multi-what” you ask? Multitasking is doing more than one thing at a time. Usually, the code you
write does one thing at a time, and doesn’t perform the next thing until the first is finished. There are,
however, techniques that enable your app to trigger a block of code that will execute in parallel, so that both blocks of code are running, more or less, concurrently. This is explained in more detail in Chapter 24. You’ve alreadydone this in your app, probably without realizing it:
[self.webView loadRequest:[NSURLRequest requestWithURL:url]];
The -loadRequest: message you sent the web view object didn’t load the URL; it simply starts the process of loading the URL. The call to this method returns immediately and your code continues on, doing other things. This is called an asynchronous method. One of those things you want to
keep doing is responding to user touches—something that’s covered in Chapter 4. This is important, because it keeps your app responsive.
Meanwhile, code that’s part of the UIWebView class started running on its own, quietly sending requests to a web server, collecting and interpreting the responses, and ultimately displaying the rendered page in the web view. This is often referred to as a background thread, or background task, because it does its work silently, and independently, of your main app (called the foreground thread).
Becoming a Web View Delegate
All of this multitasking theory is great to know, but it still doesn’t answer the question of how your
app learns when a web page has, or has not, loaded. There are several ways tasks can communicate with one another. One of those ways is to use a delegate. A delegate is an object that agrees to undertake certain decisions ortasks for another object, or would like to be notified when certain events occur. It’s this last aspect of delegates that you’ll use in this app.
The web view class has a delegate outlet. You connect that to the object that’s going to be its delegate. Delegates are a popular programming pattern in iOS. If you poke around the Cocoa Touch library, you’ll see that a lot ofclasses have a delegate outlet. Chapter 6 covers delegates in some detail.
Becoming a delegate is a three-step process:
1. In your custom class, adopt the delegate’s protocol.
2. Implement the appropriate protocol methods.
3. Connect the delegate outlet of the object to your delegate object.
A protocol is a contract, or promise, that your class will implement specific methods. This lets other objects know that your object has agreed to accept certain responsibilities. A protocol can declare two kinds of methods:required and optional. All required methods must be included in your class’s implementation. If you leave any out, you’ve broken the contract, and your project won’t compile.
It’s up to you to decide which optional methods you implement. If you implement an optional method, your object will receive that message. If you don't, it won’t. It’s that simple. Most delegate methods are optional.
The first step is to decide what object will act as the delegate and adopt the appropriate protocol.
Select your SUViewController.h file. Change the line that declares the class so it reads:
@interface SUViewController : UIViewController <UIWebViewDelegate>
The change is adding the <UIWebViewDelegate> to the end of the class declaration, between less than and greater than symbols, sometimes referred to as “angled brackets.” Adding this to your class definition means thatyour class agrees to handle messages listed in the UIWebViewDelegate protocol, and is prepared to be connected to a UIWebView’s delegate outlet.
Looking up the UIWebViewDelegate protocol, you find that it lists four methods, all of which are optional:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
- (void)webViewDidStartLoad:(UIWebView *)webView;
- (void)webViewDidFinishLoad:(UIWebView *)webView;
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error;
The first method, -webView:shouldStartLoadingWithRequest:navigationType:, is sent to the delegate whenever the user taps on a link. It allows your delegate to decide if that link should be taken. You could, for example, create aweb browser that kept the user on a particular site, like a school calendar. Your delegate could block any link that took the user to another site, or maybe just warn them that they were leaving. This app doesn’t need to doanything like that, so just ignore this method. By not implementing this method, the web view will let the user tap, and follow, any link they want.
The next three methods are the ones you’re interested in. -webViewDidStartLoad: is sent to your delegate when a web page begins to load. -webViewDidFinishLoad: is sent when it’s finished. And finally, -webView:didFailLoadWithError: is sent if the page could not be loaded for some reason.
You want to implement all three of these methods. Get started with the first one. Select your
SUViewController.m (the implementation) file, and find a place to add this method:
- (void)webViewDidStartLoad:(UIWebView *)webView
{
self.shortenButton.enabled = NO;
}
When a web page begins to load, this method will disable (by setting the enabled property to NO), the button that shortens an URL. You do this simply so the short URL button can’t be triggered between
pages, and also we’re not sure if the page can be loaded successfully yet. You’d like to limit the URL shortening to URLs you know are good.
Below that method, add this one:
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
self.shortenButton.enabled = YES;
self.urlField.text = webView.request.URL.absoluteString;
}
This method is invoked after the web page is finished loading. The first line uses the shortenButton outlet you created earlier to enable the “shorten URL” button. So as soon as the web page loads, the button to convert it to ashort URL becomes active.
The second line fixes up an issue I brought up earlier in the “Debugging” section. You want the URL in the text field at the top of the screen to reflect the page the user is looking at in the web view. This code keeps the two in sync. After a web page loads, this line digs into the webView object to find the URL that was actually loaded. The request property (an NSURLRequest) contains an URL property
(an NSURL), which has a property named absoluteString. This property returns a plain string object
(NSString) that describes the complete URL. In short, it turns an URL into a string, the reverse of what you did in -loadLocation:. The only thing left to do is to assign it to the text property of the urlField object, and the new URLappears in the text field.
The last method is received only if the web page couldn’t be loaded. It is, ironically, the most complicated method because we want to take the time to tell the user why the page wasn’t loaded—instead of just making them guess. Here’sthe code:
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
{
NSString *message = [NSString stringWithFormat:
@"A problem occurred trying to load this page: %@", error.localizedDescription];
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Could not load URL" message:message
delegate:nil cancelButtonTitle:@"That's Sad" otherButtonTitles:nil];
[alert show];
}
The first statement creates a message that says “A problem occurred…” and includes a description of the problem from the error object the web view sent along with this message. The next two statements create an alert view—a pop-up dialog—presenting the message to the user.
You’ve now done everything you need to make your SUViewController object a web view delegate, but it isn’t a delegate yet. The last step is to connect the web view to it. Select the Main_iPhone.storyboard file. Holding down thecontrol key, drag from the web view object and connect it to the View Controller. When you release the mouse button, choose the delegate outlet, as shown in Figure 3-25.
Figure 3-25. Connecting the web view delegate
Now your SUViewController object is the delegate for the web view. As the web view does its thing, your delegate receives messages on its progress. You can see this working in the simulator. Run your app, go to an URL (theexample in Figure 3-26 uses http://developer.apple.com), and now follow a link or two in the web view. As each page loads, the URL in the text field is updated.
Figure 3-26. URL field following links
Shortening an URL
You’ve finally arrived at the moment of truth: writing the code to shorten the URL. But first, let’s review what has happened so far:
n The user has entered an URL and loaded it into a web view.
n When the web view loaded, it sent your SUViewController object a
-webViewDidFinishLoad: message, where your code enabled the “shorten URL” button.
What you want to happen next is for the user to tap the “shorten URL” button and have the long URL be magically converted into a short one. That sounds like an action. Select your SUViewController.m file again andadd this new method:
- (IBAction)shortenURL: (id)sender
{
NSString *urlToShorten = self.webView.request.URL.absoluteString; NSString *urlString = [NSString
[urlToShorten
stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; shortURLData = [NSMutableData new];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL
URLWithString:urlString]];
shortenURLConnection = [NSURLConnection connectionWithRequest:request delegate:self];
self.shortenButton.enabled = NO;
}
In SUViewController.h, also add this line (just before the @end statement):
- (IBAction)shortenURL:(id)sender;
This line declares the -shortURL: method to be an action and lets Interface Builder know that it can connect objects to it.
The -shortenURL: method sends a request to the X.co URL shortening service. iOS includes a number of classes that make complicated things—like sending and receiving an HTTP request to a web server—relatively easy towrite.
Writing -shortenURL:
Begin by constructing the URL. You’ll need three pieces of information:
n The service request URL
n Your GoDaddy account key
n The long URL to shorten
The first piece of information is documented at the X.co web site. To convert a long URL into a short one, and have the service return the shortened URL as plain text, submit an URL with this format:
To construct this URL, you’ll need the values for the two placeholders, <YourAccountKey> and
<LongURL>. Get your account key from GoDaddy and use it to define the kGoDaddyAccountKey
preprocessor macro (see the X.co URL Shortening Service sidebar).
The last bit of information you need is the URL to shorten. Start with that, just as you did in
-webViewDidFinishLoad: method, and assign it to the urlToShorten variable:
NSString *urlToShorten = self.webView.request.URL.absoluteString;
The second line of code is the most complicated statement in your app. It constructs the entire URL using NSString’s +stringWithFormat: method. The first parameter is the format string, or template,for the finished string object. The two %@ sequences in the format are replaced with the values of the next two parameters. The first is the kGoDaddyAccountKey constant you defined earlier, and the second is the URL youwant shortened, currently residing in the urlToShorten variable.
Notice that the urlToShorten value isn’t used directly. Instead, it is sent the
-stringByAddingPercentEscapesUsingEncoding: message. This message replaces any characters that have special meaning in an URL with a character sequence that won’t be confused for something important. The sidebar “URLString Encoding” explains why this is done and how it works.
shortURLData = [NSMutableData new];
The third line of code might seem like a bit of a mystery. It sets an instance variable named shortURLData to a new, empty, NSMutableData object. Don’t worry about it now. It will make sense soon.
The next line of code is very similar to what you used earlier to load a web page:
NSURLRequest *request = [NSURLRequest requestWithURL:
[NSURL URLWithString:urlString]];
Just like the web view, the NSURLConnection class (the class that will send the URL for us) needs an NSURLRequest. The NSURLRequest needs an NSURL. Working backwards, this line creates an NSURL from the URL string youjust constructed, and uses that to create a new NSURLRequest object, saving the final results in the request variable.
The next statement is what does (almost) all of the work:
shortenURLConnection = [NSURLConnection connectionWithRequest:request delegate:self];
NSURLConnection’s +connectionWithRequest: creates a new NSURLConnection object and immediately starts the process of sending the requested URL. Just like the web view’s -loadRequest: method,
this is an asynchronous message—it simply starts a background task and returns immediately. And just like the web view, you supply a delegate object to receive messages about its progress, as they occur.
Unlike a web view, however, the delegate for an NSURLConnection is passed (programmatically) when you make the request. That’s what the delegate:self part of the message does; it tells NSURLConnection to use thisobject (self) as the delegate.
What’s that you say? You haven’t made the SUViewController class an URL connection delegate? You’re absolutely right, and that’s not your only problem. Xcode is also complaining that the variables shortURLData andshortenURLConnection don’t exist either, as shown in Figure 3-27. Start by fixing the missing variables.
Figure 3-27. Compiler errors in -shortenURL
Adding Private Instance Variables
The missing variables needed to be added to your SUViewController class. When receiving the information from a remote service, a couple of pieces of information must be maintained while that happens. These are theNSURLConnection object, that’s doing the work, and an NSMutableData object, that will collect the data sent back from the web server.
These variables, however, are not for public consumption; they don’t need to be accessed by other objects, or connected in Interface Builder. Simply put, these are private variables. You create private variables by declaring themin the private interface of the SUViewController. Scroll to the beginning of the SUViewController.m file and find the @interface SUViewController () section. Change it so it looks like this (new code in bold):
@interface SUViewController ()
{
NSURLConnection *shortenURLConnection; NSMutableData *shortURLData;
}
@end
As soon as you add this, the warnings you saw in -shortenURL: will go away.
Becoming an NSURLConnection Delegate
You can now follow the same steps you took to make SUViewController a delegate of the web view, to turn it into an NSURLConnection delegate as well. There’s no practical limit on how many objects your object can be adelegate for.
Step one is to adopt the protocols the make your class a delegate. NSURLController declares a couple of different delegate protocols, and you’re free to adopt the ones that make sense to your
app. In this case, you want to adopt the NSURLConnectionDelegate and NSURLConnectionDataDelegate protocols. Do this by adding those protocol names to your SUViewController class, in your SUViewController.h file, like this:
@interface SUViewController : UIViewController <UIWebViewDelegate, NSURLConnectionDelegate, NSURLConnectionDataDelegate>
The NSURLConnectionDelegate defines methods that get sent to your delegate when key events occur. There are a slew of messages that deal with how your app responds to authenticated content (files on the web server that areprotected by an account name and password). None of that applies
to this app. The only message you’re interested in is -connection:didFailWithError:. That message is sent if the request fails for some reason. Open your SUViewController.m file and add this new method:
- (void)connection:(NSURLConnection *)connection
didFailWithError:(NSError *)error
{
self.shortLabel.title = @"failed"; self.clipboardButton.enabled = NO; self.shortenButton.enabled = YES;
}
It’s unlikely that an URL shortening request would fail. The only likely cause would be that your iPhone has temporarily lost its Internet connection. Nevertheless, you want your app to behave itself, and do something intelligent,under all circumstances. This method handles a failure by doing three things:
n Sets the short URL label to “failed”, indicating that something went wrong
n Disables the “copy to clipboard” button, because there’s nothing to copy
n Turns the “shorten URL” button back on, so the user can try again
With the unlikely stuff taken care of, let’s get to what should happen when you send a request. The NSURLConnectionDataDelegate protocol methods are primarily concerned with how your app gets the data returned from theserver. It, too, defines a bunch of other methods you’re not interested in.
The two you are interested in are -connection:didReceiveData: and -connectionDidFinishLoading:.
Start by adding this -connection:didReceiveData: method to your implementation:
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[shortURLData appendData:data];
}
The X.co service returns the shortened URL in the body of the HTTP response, as a simple string of ASCII characters. Your delegate object will receive a -connection:didReceiveData: message every time new body data has beenreceived from the server. In this app, that’s probably only going to be once, since the amount of data you’re requesting is so small. If your app requested a lot of data (like an entire web page), this message would be sent multiple times.
The only thing this method does it take that data that was received (in the data parameter), and adds it to the buffer of data you’re maintaining in shortURLData. Remember the shortURLData = [NSMutableData new];statement back in -shortenURL:? That statement set up an empty buffer (NSMutableData) before the request was started. As you receive the answer to that request, it accumulates in your shortURLData variable. Does that allmake sense? Let’s move on to the final method.
The last method should be self-explanatory by now. The -connectionDidFinishLoading: message is sent when the transaction is complete: you’ve sent the URL request, received all of the data, and the whole thing was a success.Add this method to your implementation:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSString *shortURLString = [[NSString alloc] initWithData:shortURLData encoding:NSUTF8StringEncoding];
self.shortLabel.title = shortURLString; self.clipboardButton.enabled = YES;
}
The first statement turns the ASCII bytes you received in -connection:didReceiveData: and turns them into a string object. String objects use Unicode character values, so turning a string of bytes into a string of charactersrequires a little conversion.
The second line sets the title of the shortLabel toolbar button to the short URL you just received (and converted). This makes the short URL appear at the bottom of the screen.
The last step is to turn on the “copy to clipboard” button. Now that your app has a valid short URL, it has something to copy.














No comments:
Post a Comment