Saturday, 26 April 2014

Setting Action Connections [Spin a Web]

Lets 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

Whats missing? The question is “how does the -loadLocation: method get invoked?” Thats 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 SUViewControllers 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 Apples home page appears in the web view. Thats 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 whats 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 doesnt 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 havent 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 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, its time to roll up your sleeves and write the code that will make this work. Heres 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 doesnt 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 didnt 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 thats covered in Chapter 4. This is important, because it keeps your app responsive.

Meanwhile, code thats 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 doesnt 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. Its 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 thats 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 delegates 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 classs implementation. If you leave any out, you’ve broken the contract, and your project wont compile.

Its 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 wont. Its 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 UIWebViewdelegate  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 calendarYour delegate could block any link that took the user to another site, or maybe just warn them that they were leaving. This app doesnt 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 its 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 cant 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 wepage couldnt be loadedIt is, ironically, the most complicatemethod because we want to take the time to tell the user why the page wasnt loaded—instead of jusmaking them guess. Heresthe 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 isnt 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, letreview 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 
stringWithFormat:@"http://api.x.co/Squeeze.svc/text/%@?url=%@", kGoDaddyAccountKey,
[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:

http://api.x.co/Squeeze.svc/text/<YourAccountKey>?url=<LongURL>

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 +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 isnt 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 wont bconfused for somethinimportant. 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. Dont 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 NSURLWorking 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+connectionWithRequest: creates a new NSURLConnection object and immediately starts the process of sending the requested URL. Just like the web views -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. Thats what the delegate:self part of the message does; it tells NSURLConnection to use thisobject (self) as the delegate.

Whats that you say? You havent made the SUViewController class an URL connection delegate? You’re absolutely right, and thats not your only problem. Xcode is also complaining that the variables shortURLData andshortenURLConnection dont 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, thats 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 dont 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. Theres 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 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 youSUViewController.h file, like this:

@interface  SUViewController : UIViewController <UIWebViewDelegateNSURLConnectionDelegate, 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;
}

Its 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 theres nothing to copy

n     Turns the “shorten URL” button back on, so the user can try again

With the unlikely stuff taken care of, lets 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, thats 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? Lets 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