Retrieve deleted data in iOS

This is the original translation of Chapter 6 Retrieving remote data from iOS7 in Action . Unlike the book, the whole interface is made programmatically, accordingly the text that describes how to do all this in the storyboard is removed. For simplicity, the only Portrait screen position and iPhone target platform have been selected.

We will create an application with a single Label on the screen, which will display a random joke about Chuck Norris, downloaded via the api.icndb.com/jokes/random site API at the time the application was launched.


Fig. 1 Our app showing a joke about Chuck Norris.

Retrieving Data Using NSURLSession


HTTP request theory

For example, recall how HTTP interaction occurs. Suppose we type in the URL the browser: google.com/?q=Hello&safe=off.

The following components can be distinguished in it:

  • http is a protocol that tells the browser to follow the HTTP standard when requested
  • : // separates the protocol from the domain
  • google.com - domain from where we get the data
  • / - request path that determines the position of the resource we are defining
  • ? used to separate the path from the parameter
  • q = Hello & safe = off - parameters. Each parameter consists of a key-value pair. The q key is Hello, the safe key is off

When making an HTTP request, we always specify a method. The method determines what the server needs to do with the information sent by us.

In short, the browser connects to google.com and makes a GET request corresponding to the HTTP protocol to the root directory /, passing q = Hello & safe = off as the parameter.

When the browser parses the URL, the HTTP request will be presented like this:

GET /?q=Hello&safe=off HTTP/1.1
Host: google.com
Content-Length: 133
(…)

The request consists of ASCII text strings. The first line contains the GET method, followed by the path with parameters, then the HTTP version. This is followed by a header containing details such as the requested host and request length. The header is separated from the body by two blank lines. For GET, the body is empty.

Figure 2 shows a query diagram. First, a request is created, then a connection is established to the remote server, and the request is sent in plain text. Depending on the size of the request and the quality of the network, the request can last seconds, hours, or even days.


Figure 2 Steps in an HTTP request: (a) normal HTTP interaction and (b) equivalents for each step from the point of view of the Objective-C implementation.

We create the program interface of our application

Create an Empty Application in Xcode and the following files in it:

THSAppDelegate.h

#import 
@class THSViewController;
@interface THSAppDelegate : UIResponder 
@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) THSViewController *viewController;
@end

THSAppDelegate.m

#import "THSAppDelegate.h"
#import "THSViewController.h"
@implementation THSAppDelegate
- (BOOL)          application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    self.viewController = [[THSViewController alloc] initWithNibName:nil bundle:nil];
    self.window.rootViewController = self.viewController;
    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    return YES;
}
@end

THSViewController.h

#import 
@interface THSViewController : UIViewController
@property (nonatomic, strong) UILabel *jokeLabel;
@end

THSViewController.m

#import "THSViewController.h"
#import "THSViewController+Interface.h"
@implementation THSViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
    [self addLabel];
}
@end

THSViewController + Interface.h

#import "THSViewController.h"
@interface THSViewController (Interface)
- (void)addLabel;
@end

THSViewController + Interface.m

#import "THSViewController+Interface.h"
@implementation THSViewController (Interface)
- (void)addLabel
{
    CGFloat width = self.view.frame.size.width - 40.0f;
    CGFloat y = self.view.frame.size.height / 2.0f - 200.0f;
    CGRect labelFrame = CGRectMake(20.0f, y, width, 200.0f);
    self.jokeLabel = [[UILabel alloc] initWithFrame:labelFrame];
    self.jokeLabel.text = @"Quotation goes here and continues and continues until I am fed up to type.";
    self.jokeLabel.lineBreakMode = NSLineBreakByWordWrapping;
    self.jokeLabel.textAlignment = NSTextAlignmentCenter;
    self.jokeLabel.numberOfLines = 0;
    self.jokeLabel.font = [UIFont systemFontOfSize:16.0f];
    [self.view addSubview:self.jokeLabel];
}
@end

Creating an interface is categorized so as not to confuse the main functionality of the chapter with the label code and buttons in the future.

We launch the application and see the interface: Fig .


3 Starting application interface.

Create Cocoa class THSHTTPCommunication

All UI operations are performed in the main thread. If you block the main thread, then all touch events will be blocked, drawing graphics, animation, sounds, eventually the application will freeze. Therefore, you cannot just interrupt the application to wait for a request. To solve this problem, there are two techniques: create a new thread for managing two simultaneous operations or configure the class instance as a delegate and implement the methods defined in the delegate protocol.

Most methods in Cocoa were originally designed as a delegate pattern to make time-consuming operations asynchronous. This means that the main thread will continue until the operation is completed, after which a certain method will be called.

With iOS 5, Objective-C supports blocks.

We will use the delegates in this example to gain a deeper understanding of networking. But remember that iOS7 has syntactic sugar that uses blocks to simplify the execution of HTTP requests.

Create the THSHTTPCommunication class.

THSHTTPCommunication.h:

#import 
@interface THSHTTPCommunication : NSObject
@end

THSHTTPCommunication.m:

#import "THSHTTPCommunication.h"
@interface THSHTTPCommunication ()
@property(nonatomic, copy) void(^successBlock)(NSData *);
@end
@implementation THSHTTPCommunication
@end

where successBlock contains the block that will be called when the request completes.

We implement HTTP interaction

Next, create a method in THSHTTPCommunication.m, responsible for the HTTP interaction. This method pulls jokes from a public API called icndb and returns information asynchronously using blocks.

THSHTTPCommunication.m:

@implementation THSHTTPCommunication
- (void)retrieveURL:(NSURL *)url successBlock:(void(^)(NSData *))successBlock
{
    // сохраняем данный successBlock для вызова позже
    self.successBlock = successBlock;
    // создаем запрос, используя данный url
    NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
    // создаем сессию, используя дефолтную конфигурацию и устанавливая наш экземпляр класса как делегат
    NSURLSessionConfiguration *conf =
        [NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:conf delegate:self delegateQueue:nil];
    // подготавливаем загрузку
    NSURLSessionDownloadTask *task = [session downloadTaskWithRequest:request];
    // устанавливаем HTTP соединение
    [task resume];
}
@end

This method takes two parameters: the URL, where we get the content from (in our case, this is the icndb API URL for receiving random jokes) and the block that will be called immediately after the request is completed. First you need to save this block to call later when the request is completed. The next step is to create an NSURLRequst object for this URL and use this request to establish an HTTP connection. [task resume] will not block execution. In this method, the compiler will show warning, because we have not yet reported that the THSHTTPCommunication class conforms to the NSURLSessionDownloadDelegate protocol. This is what we will do next.

Session delegate

We implement the NSURLSessionDownloadDelegate protocol to catch some of the messages, such as when we receive a new request.

First let the compiler know that THSHTTPCommunication obeys this protocol.

THSHTTPCommunication.h:

@interface THSHTTPCommunication : NSObject
@end

The NSURLSessionDownloadDelegate protocol defines a set of methods that an instance of the NSURLConnection class can perform during an HTTP connection. We use URLSession: downloadTask: didFinishDownloadingToURL:

There are two more methods that we may need for more complex cases, such as tracking the download process and the ability to resume the request. The names speak for themselves:

URLSession: downloadTask: didResumeAtOffset: expectedTotalBytes:
URLSession: downloadTask: didWriteData: totalBytesWritten: totalBytesExpectedToWrite:


But that's not all. In addition, the NSURLSession API provides three protocols:

NSURLSessionDelegate- This protocol defines delegate methods for handling session-level events as revoking a session or credentials.
NSURLSessionTaskDelegate - This protocol defines delegate methods for handling connection events as redirects, errors, and data forwarding.
NSURLSessionDataDelegate - This protocol defines delegate methods for handling task-level events specific to data and load tasks.

We receive data from the answer

Now we implement a method that will allow you to receive data from the response in THSHTTPCommunication.m. NSURLSession will call this method as soon as the data is available and the download is finished.

THSHTTPCommunication.m:

@implementation THSHTTPCommunication
…
- (void) URLSession:(NSURLSession *)session
       downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
    // получаем загруженные данные из локального хранилища
    NSData *data = [NSData dataWithContentsOfURL:location];
    // гарантируем, что вызов successBlock происходит в главном потоке
    dispatch_async(dispatch_get_main_queue(), ^{
        // вызываем сохраненный ранее блок как колбэк
        self.successBlock(data);
    });
}
@end

This code is the last step in the interaction. We get the full answer and immediately call the block that we saved earlier. First we get the locally stored data that we received from the server. We notice that the repository is represented by an instance of the NSURL class, but this time the URL is the path to the file in which the response data is contained, and not the remote URL.

We need to be sure that the successBlock callback call comes from the main thread. This is common practice, because most likely the method that implements the class does the main task-specific tasks, such as UI actions.

In some cases, when we receive information from a remote server, the request may go through several servers before reaching the destination. Fig. 4 We are trying to get a picture located ont.co, but the first answer is a redirect to the server containing the picture. We need only the last answer (the picture itself).


Fig. 4 Receiving a picture using the abbreviated Twitter url generates a redirect.

Although we can control redirects by implementing NSURLSessionTaskDelegate, we can let NSURLSession handle all the details, which is the default behavior.

We make available the newly created retrieveURL: successBlock: method for the main controller. Open THSHTTPCommunication.h and add the method declaration:

THSHTTPCommunication.h:

@interface THSHTTPCommunication : NSObject 
- (void)retrieveURL:(NSURL *)url successBlock:(void(^)(NSData *))successBlock;
@end

Understanding data serialization and interactions with third-party services


We created the application and wrote the logic for receiving data from a remote server. Now get some random jokes about Chuck Norris using the APIs provided by icndb.com. This API, like all services that provide interaction with third-party services, has a normalized way of formatting information. Therefore, you need to transform this format into something simple to use and manipulate. In other words, you need a way to convert formatted data to Objective-C objects.

Serialization

Figure 5 illustrates how the serialization process works. We see the sender (icndb server) on the left and the receiver (client) on the right. First a joke is generated, icndb saves it as binary data (it can be saved as a database, memory, file system or any other kind of storage). When a request from the application occurs, information about the joke is serialized and sent to us (the recipient). The application parses information and converts the received data into native Objective-C objects.


Fig. 5 Architecture of serialization and deserialization of messages.

There are various ways to share information, but we will focus on the most widely used serialization format: JavaScript Object Notation (JSON). JSON is a standard way of representing various types of data structures in text form. JSON defines a small set of rules for representing strings, numbers, and Boolean values. Together with XML, this is one of the most used serialization methods today. Let's look at an example of JSON in action:

{
"name": "Martin Conte Mac Donell",
"age": 29,
"username": "fz"
}

This code represents a dictionary enclosed in {} and consisting of key / value pairs. Keys cannot be repeated. In our example, name, age and username are keys, Martin Conte Mac Donell, 29 and fz are values.

Joke Code

Now we know that the JSON format is defined and how the serialization process works, back to the application. We implement the code to get jokes. In THSViewController.m we add an internal variable jokeID an unnamed category and the retrieveRandomJokes method.

THSViewController.m:

#import "THSViewController.h"
#import "THSViewController+Interface.h"
#import "THSHTTPCommunication.h"
@interface THSViewController ()
{
    NSNumber *jokeID;
}
@end
@implementation THSViewController
...
- (void)retrieveRandomJokes
{
    THSHTTPCommunication *http = [[THSHTTPCommunication alloc] init];
    NSURL *url = [NSURL URLWithString:@"http://api.icndb.com/jokes/random"];
    // получаем шутки, используя экземпляр класса THSHTTPCommunication
    [http retrieveURL:url successBlock:^(NSData *response)
    {
        NSError *error = nil;
        // десериализуем полученную информацию
        NSDictionary *data = [NSJSONSerialization JSONObjectWithData:response
                                                             options:0
                                                               error:&error];
        if (!error)
        {
            NSDictionary *value = data[@"value"];
            if (value && value[@"joke"])
            {
                jokeID = value[@"id"];
                [self.jokeLabel setText:value[@"joke"]];
            }
        }
    }];
}
@end

We define the retrieveRandomJokes method and see how serialization happens from a code perspective. In this method, we use the previously created THSHTTPCommunication class to retrieve data from icndb.com. Therefore, we immediately create an instance of the THSHTTPCommunication class and then call retrieveURL: successBlock :, which is responsible for receiving the data. As soon as THSHTTPCommunication receives a response from icndb.com, it calls the code inside the block passed as a parameter. At this point, we have available data ready for parsing.

When information is received, it needs to be understood. You need a way to convert just downloaded text into something that can be easily manipulated. You need to highlight the joke and id from the answer. The process of converting serialized data (JSON) to data structures is called deserialization. Fortunately, starting with iOS 5, the Cocoa framework includes a class for parsing JSON. This is an NSJSONSerialization class, and parsing response data is the first thing we do in a block.

The answer from icndb API is an associative array represented with JSON as

{
"type": "success",
"value": 
  {
   "id": 201,
   "joke": "Chuck Norris was what Willis was talkin’ about"
   }
}

We see that the answer is an associative array and the key “value” contains another associative array. Once NSJSONSerialization completes deserialization, the JSON associative array will be converted to Objective-C NSDictionaries, arrays to NSArray, numbers to NSNumber, and strings to NSString. After that, we get an object that can be used in the application.

Returning to retrieveRandomJokes :, after deserialization, we assign an associative array from the “value” key of the deserialized response to the NSDictionary dictionary. Finally, we make the received joke text as label text in our interface.

It remains to call the retrieveRandomJokes: method when the view has loaded.

THSViewController.m:

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self addLabel];
    [self retrieveRandomJokes];
}

That's all, now run the application and see a new joke at every launch.

Also popular now: