Pull to refresh

Получение удаленных данных в iOS. Часть 2

Reading time 10 min
Views 9K
Это продолжении статьи, которая является авторским переводом главы 6 Retrieving remote data из книги iOS7 in Action.

Продвинутые HTTP запросы


Пока что мы использовали только метод GET, но доступны и другие методы:
  • POST
  • PUT
  • DELETE
  • OPTIONS
  • HEAD
  • TRACE
  • CONNECT

Мы сосредоточимся на двух наиболее популярных методах: GET и POST.

GET – это простейший метод HTTP запроса, и именно его использует браузер для загрузки веб-страниц. Он используется для запроса содержимого, расположенного по определенному URL. Содержимое может быть, например, веб-страницей, рисунком или аудио-файлом. По соглашению, GET запросы осуществляют только чтение и в соответствии с W3C стандартом не должны быть использованы в операцих, изменяющий серверную сторону. Например, мы не будем использовать GET запрос для отсылания формы или пересылки фотографии, потому что эти операции требуют некоторых изменений на серверной стороне (мы будем использовать в этих случаях POST).

POST посылает данные для дальнейшей обработки на URL. Параметры включены в тело запроса, использующего тот же формат, что и GET. Например, если мы хотим запостить форму, содержающую два поля, имя и возраст, то мы пошлем что-то похожее на name=Martin&age=29 в теле запроса.

Такой способ пересылки параметров широко используется в веб-страницах. Наиболее популярные случаи – это формы. Когда мы заполняем форму на сайте и кликаем Submit, вероятнее всего запрос будет POST.

Давайте вернемся к нашему приложению и используем часть полученных знаний. А именно, давайте используем POST для выставления рейтинга шуток. Мы будем посылать голоса (или +1 или -1) на удаленный сервер.

Сначала напишем необходимый интерфейс. Добавим две кнопки для голосования voteUp и voteDown, которые соответственно будут повышать или понижать рейтинг текущей шутки. Также добавим кнопку “Chuck Who?”, которую мы наполним функционалом в разделе про web view. В THSViewController+Interface.h добавим декларации этих методов.

THSViewController+Interface.h
#import "THSViewController.h"

@interface THSViewController (Interface)

- (void)addLabel;
- (void)addButtonVoteUp;
- (void)addButtonVoteDown;
- (void)addButtonChuckWho;

@end


В THSViewController+Interface.m реализуем эти методы.

THSViewController+Interface.m
- (void) addButtonVoteUp
{
    UIButton *voteUpButton = [UIButton buttonWithType:UIButtonTypeSystem];
    [voteUpButton setTitle:@"Vote Up" forState:UIControlStateNormal];
    CGFloat x = self.view.frame.size.width / 2.0 - 50.0f;
    CGFloat y = self.view.frame.size.height / 2.0 + 0.0f;
    voteUpButton.frame = CGRectMake(x, y, 100.0f, 50.0f);

    [voteUpButton addTarget:self
                     action:@selector(voteUp)
           forControlEvents:UIControlEventTouchUpInside];

    [self.view addSubview:voteUpButton];
}

-(void) addButtonVoteDown
{
    UIButton *voteDownButton = [UIButton buttonWithType:UIButtonTypeSystem];
    [voteDownButton setTitle:@"Vote Down" forState:UIControlStateNormal];
    CGFloat x = self.view.frame.size.width / 2.0 - 50.0f;
    CGFloat y = self.view.frame.size.height / 2.0 + 50.0f;
    voteDownButton.frame = CGRectMake(x, y, 100.0f, 50.0f);

    [voteDownButton addTarget:self
                     action:@selector(voteDown)
           forControlEvents:UIControlEventTouchUpInside];

    [self.view addSubview:voteDownButton];
}

- (void)addButtonChuckWho
{
    UIButton *chuckWhoButton = [UIButton buttonWithType:UIButtonTypeSystem];
    [chuckWhoButton setTitle:@"Chuck Who?" forState:UIControlStateNormal];
    CGFloat x = self.view.frame.size.width / 2.0 - 50.0f;
    CGFloat y = self.view.frame.size.height / 2.0 + 150.0f;
    chuckWhoButton.frame = CGRectMake(x, y, 100.0f, 50.0f);

    [chuckWhoButton addTarget:self
                     action:@selector(chuckWho)
           forControlEvents:UIControlEventTouchUpInside];

    [self.view addSubview:chuckWhoButton];
}


В THSViewController.h добавим декларации экшенов для этих кнопок. voteUp для addButtonVoteUp, voteDown для addButtonVoteDown и chuckWho для addButtonChuckWho.

THSViewController.h
#import <UIKit/UIKit.h>

@interface THSViewController : UIViewController

@property (nonatomic, strong) UILabel *jokeLabel;

- (void)voteUp;
- (void)voteDown;
- (void)chuckWho;

@end


В THSViewController.m добавляем стабы этих методов.

THSViewController.m
- (void)voteUp
{

}

- (void)voteDown
{

}

- (void)chuckWho
{
 
}


Наконец, в viewDidLoad методе THSViewController.m вызываем методы, реализующие наш интерфейс.

THSViewController.m
- (void)viewDidLoad
{
    [super viewDidLoad];

    [self addLabel];
    [self addButtonVoteUp];
    [self addButtonVoteDown];
   [self addButtonChuckWho];

    [self retrieveRandomJokes];
}


Запускаем наше приложение и видим интерфейс как на Рис. 1


Рис.1 Интерфейс нашего приложения с добавленным кнопками для голосования

Теперь реализуем необходимый функционал.

Сначала мы реализуем функционал для совершения POST запросов в классе, ответственном за все наши HTTP-операции: THSHTTPCommunication класс. Для этого в THSHTTPCommunication.m добавим новый метод postURL:params:successBlock, который похож на предыдущий retrieveURL:successBlock метод.

THSHTTPCommunication.m
- (void)postURL:(NSURL *)url params:(NSDictionary *)params
   successBlock:(void(^)(NSData *))successBlock
{
    self.successBlock = successBlock;

    // создаем временный массив для хранения POST параметров
    NSMutableArray *paramsArray = [NSMutableArray arrayWithCapacity:[params count]];

    // добавляем параметры во временной массив как key=value строку
    for (NSString *key in params)
    {
        [paramsArray addObject:[NSString stringWithFormat:@"%@=%@", key, params[key]]];
    }

    //создаем строку из массива параметров, содержащую все параметры, разделенные символом &
    NSString *postBodyString = [paramsArray componentsJoinedByString:@"&"];

    // конвертируем NSString в NSData объект, который будет использован в запросе
    NSData *postBodyData = [NSData dataWithBytes:[postBodyString UTF8String]
                                          length:[postBodyString length]];

    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
    // выставляем метод запроса в POST
    [request setHTTPMethod:@"POST"];
    // выставляем content-type как form encoded
    [request setValue:@"application/x-www-form-urlencoded"
   forHTTPHeaderField:@"content-type"];
    // добавляем созданное ранее POST тело в запрос
    [request setHTTPBody:postBodyData];

    NSURLSessionConfiguration *conf = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:conf delegate:self delegateQueue:nil];
    NSURLSessionDownloadTask *task = [session downloadTaskWithRequest:request];

    [task resume];
}


Данные POST запроса могут быть структурированы с использованием разных форматов. Параметры обычно отформатированы в соответствии со стандартами form-url-кодирования (в соответствии с W3C HTML стандартом). Этот формат дефолтный и широко используется во многих браузерах. Наш метод принимает словарь NSDictionary в качестве аргумента, но мы не может послать по HTTP соединению словарь NSDictionary, потому что это внутренний тип Objective-C. Для пересылки по HTTP-соединению нам надо создать распознаваемое представление словаря. Это как общение с иностранцем. Мы переводим наше осообщение на универсальный язык, а он переводит с универсального языка уже на свой родной как показано на Рис. 2. Универсальнйй язык в HTTP — это W3C стандарт, наш язык — это Objective-C, язык получателя нам неизвестен.

W3C стандарт определяет правила, определяющие, что означает распознаваемое представление для каждого случая. В нашем случае нам нужно представить параметры, следующие form-url-закодированной части стандарта (например, param1=var1&param2=var2).


Рис.2 Метафора передачи сообщений

Вернемся к нашему методу и посмотрим, как можно перевести все сказанное выше в код. Сначала мы создадим массив, содержащий все пары ключ-значение, которые мы потом соединим с помощью &. Полученная строка, сконвертированная в экземпляр класса NSData, — то, что мы пошлем на сервер, ответственный за хранение голосования. Для выполнения POST запроса, используя экземпляр класса NSURLRequest, мы установим HTTPMethod в POST так же, как и content-type.

Нам нужно сделать этот метод доступным, чтобы использовать его в THSViewController классе.

THSHTTPCommunication.h
@interface THSHTTPCommunication : NSObject <NSURLSessionDownloadDelegate>

- (void)retrieveURL:(NSURL *)url successBlock:(void(^)(NSData *))successBlock;
- (void)postURL:(NSURL *)url params:(NSDictionary *)params
   successBlock:(void(^)(NSData *))successBlock;

@end


Теперь THSHTTPCommunication класс включает метод для выполнения POST запроса, нам нужно вызвать его из главного THSViewController. Настало время реализовать методы повышение или понижение рейтинга, как показано в следующем листинге.

THSViewController.m
- (void)voteUp
{
    NSURL *url = [NSURL URLWithString:@"http://example.com/rater/vote"];
    THSHTTPCommunication *http = [[THSHTTPCommunication alloc] init];
    NSDictionary *params = @{@"joke_id":jokeID, @"vote":@(1)};
    [http postURL:url params:params successBlock:^(NSData *response)
     {
         NSLog(@"Voted Up");
     }];
}

- (void)voteDown
{
    NSURL *url = [NSURL URLWithString:@"http://example.com/rater/vote"];
    THSHTTPCommunication *http = [[THSHTTPCommunication alloc] init];
    NSDictionary *params = @{@"joke_id":jokeID, @"vote":@(-1)};
    [http postURL:url params:params successBlock:^(NSData *response)
     {
         NSLog(@"Voted Down");
     }];

}


Создаем объект NSURL с url, который будет использован в запросе. Создаем объект THSHTTPCommunication. Определяем параметры для запроса. Делаем POST запрос и выставляем колбэк successBlock.

Эти функции очень похожи друг на друга. Сначала определяем полный URL, который будем использовать для запроса, и затем создадим экземпляр класса THSHTTPCommunication. То же самое мы сделали перед GET запросом. Теперь мы создаем NSDictionary для хранения параметров. Переведем эти параметры в изученный ранее формат (например, joke_id=&vote=1), чтобы смочь включить их в POST запрос. Как мы видели ранее, метод, ответственный за эту трансформацию, postURL:params:successBlock из экземпляра класса THSHTTPCommunication. И делаем запрос вызывая этот метод.

Запускаем наше приложение и проверяем, что при голосовании за шутку или против нее в консоли выдаются сообщения:

2015-11-08 14:51:20.724 Test[1248:80442] Voted Up


или

2015-11-08 14:51:57.896 Test[1273:81833] Voted Down


что свидетельствует об успешно выполненном POST-запросе на сервер.

Мы написали приложение для получения шуток с использованием icndb API и GET HTTP глагола. Мы смогли показать эти шутки на UIView и каждую шутку можно оценить. Эти действия посылают POST запрос на удаленный сервер, который должен сохранить нашу оценку.

Использование web views для отображения удаленных страниц


Мы научились делать запросы на удаленный сервер. Именно это делают браузеры перед отображением веб-бстраницы. Отличие только в содержимом ответа. Веб-страницы форматированны с помощью HTML стандарта, который определяет ряд правил на то, как графически определить различные теги разметки. Эти правила кажутся простыми, но отображение целой страницы, следующей W3C стандарту – это сложная задача. К счастью, в iOS есть встроенный компонент UIWebView, который использует хорошо известный движок WebKit и интерпретирует HTML/CSS/JavaScript и отображает целые веб-страницы внутри UIView.

Вернемся к нашему приложению. Мы будем добавлять webView для отображения страницы на википедии о Чаке Норрисе. Она запутится по нажатию кнопки.

Сначала создаем класс THSWebViewController, являющийся подклассом UIViewController.

THSWebViewController.h
#import <UIKit/UIKit.h>

@interface THSWebViewController : UIViewController

@property (nonatomic, strong) UIWebView *webView;

- (void)dismissView; // скрывает нашу модальное вьюху
- (void)back;	    // осуществляет навигацию по истории запросов в браузере назад
- (void)forward;     // и вперед

@end


THSWebViewController.m
#import "THSWebViewController.h"
#import "THSWebViewController+Interface.h"

@interface THSWebViewController ()

@end

@implementation THSWebViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    [self addWebView];
    [self addNavBar];
    [self addTabBar];
}

- (void)dismissView
{
    // стаб
}

- (void)back
{
    // стаб
}

- (void)forward
{
    // стаб
}

@end


Создаем категорию THSWebViewController+Interface, в которую мы вынесли код по реализации интерфейса веб-вьюхи:

THSWebViewController+Interface.h
#import "THSWebViewController.h"

@interface THSWebViewController (Interface)

- (void)addWebView;
- (void)addNavBar;
- (void)addTabBar;

@end


THSWebViewController+Interface.m
#import "THSWebViewController+Interface.h"

@implementation THSWebViewController (Interface)

- (void)addWebView
{
    self.webView = [[UIWebView alloc] initWithFrame:self.view.frame];
    [self.view addSubview:self.webView];
}

- (void)addNavBar
{
    CGFloat width = self.view.frame.size.width;
    CGRect frame = CGRectMake(0.0f, 0.0f, width, 64.0f);
    UINavigationBar *navBar = [[UINavigationBar alloc] initWithFrame:frame];

    self.navigationItem.title = @"Chuck Norris";
    [navBar pushNavigationItem:self.navigationItem animated:NO];

    self.navigationItem.leftBarButtonItem =
    [[UIBarButtonItem alloc] initWithTitle:@"Back"
                                     style:UIBarButtonItemStylePlain
                                    target:self
                                    action:@selector(dismissView)];

    [self.view addSubview:navBar];
}

- (void)addTabBar
{
    CGFloat width = self.view.frame.size.width;
    CGRect frame = CGRectMake(0.0f, self.view.frame.size.height - 44.0f, width, 44.0f);
    UIToolbar *toolBar = [[UIToolbar alloc] initWithFrame:frame];

    frame = CGRectMake(0.0f, 0.0f, 50.0f, 30.0f);
    UIBarButtonItem *backBtn =
    [[UIBarButtonItem alloc] initWithTitle:@"<"
                                     style:UIBarButtonItemStylePlain
                                    target:self
                                    action:@selector(back)];
    UIBarButtonItem *forwardBtn =
    [[UIBarButtonItem alloc] initWithTitle:@">"
                                     style:UIBarButtonItemStylePlain
                                    target:self
                                    action:@selector(forward)];

    [toolBar setItems:@[backBtn, forwardBtn]];

    [self.view addSubview:toolBar];
}

@end


Теперь в THSViewController.m добавляем реализацию метода chuckWho, декларацию которого мы сделали ранее. Не забываем также импортировать THSWebViewController.h файл.

THSViewController.m
#import "THSViewController.h"
#import "THSViewController+Interface.h"
#import "THSHTTPCommunication.h"
#import "THSWebViewController.h"

…

- (void)chuckWho
{
    THSWebViewController *webViewController = [[THSWebViewController alloc] init];

    [self presentViewController:webViewController animated:YES completion:nil];
}


Теперь при нажатии на кнопку Chuck Who, мы видим пустую web view как на Рис.3.


Рис.3. Пустая web view в модальном окне

Заполняем ее функционалом. Для этого в viewDidLoad класса THSWebViewController добавляем следующий код.

THSWebViewController.m
- (void)viewDidLoad
{
    [super viewDidLoad];

    [self addWebView];
    [self addNavBar];
   [self addTabBar];

    // создаем объект NSURLRequest с URL на страницу Википедии о Чаке Норрисе
    NSURLRequest *request = [NSURLRequest requestWithURL:
                             [NSURL URLWithString:@"http://en.wikipedia.org/wiki/Chuck_Norris"]];
   // исполняем запрос с помощью экземпляра класса webView
    [self.webView loadRequest:request];
}


Как и ранее мы создали объект NSURLRequest, но вместо того, чтобы посылать запрос с помощью NSURLSession, мы используем метод loadRequest класса UIWebView, который делает всю работу за нас.

Наконец, осталось реализовать функционал методов dismissView, back и forward.

THSWebViewController.m
- (void)dismissView
{
	// скрывает webView с экрана, когда вызван метод close
    [self dismissViewControllerAnimated:YES completion:nil];
}

- (void)back
{
	// загружает предыдущую локацию в истории, когда нажата кнопка back
    [self.webView goBack];
}

- (void)forward
{
	// загружает следующую локацию в истории, когда нажата кнопка forward
    [self.webView goForward];
}


Теперь, когда все завершено, важно знать еще несколько вещей о UIWebView.

Есть несколько случаев, когда вы хотите контролировать поток навигации. Например, вы хотите знать, когда определенный контент или определенный URL загружен.

Или возможно вы реализуете безопасный браузер для детей, вы хотите заблокировать пользователя от загрузки страниц, попадающих под определенные критерии, как секс или наркотики. Для всех таких типов кастомизации вы создаете экземпляр класса, который реализует UIWebViewDelegate протокол в качестве делегата UIWebView. Вы можете реализовать следующие методы:
webView:shouldStartLoadWithRequest:navigationType:
webViewDidStartLoad:
webViewDidFinishLoad:
webView:didFailLoadWithError:


С помощью первого метода вы можете контролировать поток навигации, разрешая или блокируя специфические запросы. Остальные три метода – информационные события (имена метода дадут вам хорошее представление о событии).

На этом все! Рис. 4 демонстрирует как web view должно выглядить в вашем приложении.


Рис. 4. Финальный вид реализованного нами web view
Tags:
Hubs:
+6
Comments 1
Comments Comments 1

Articles