Objective-C: HTTP запрос (POST/GET) и распарсивание JSON

Screen Shot 2017-04-22 at 00.35.00

Язык Swift растет быстро и становится изящным языком программирования, но еще не время списывать со счетов Objective-C. Причина тому в том, что корпоративный мир все еще пишет iOS приложения на нем.

Таким образом я решил написать маленькую серию постов касательно использования Objective-C. Предлагаю начать с той функциональности, без которого сейчас не обходится большинство мобильных приложений: HTTP запрос. Мы сделаем GET запрос для получения текущей погоды в Праге и распарсим JSON полученный в ответ.

Шаг 1.

Создаем «Single View Application». Не забываем поставить правильный язык.

Screen Shot 2017-04-19 at 19.11.50

Шаг 2.

Создайте API ключ как указано в этом посте (Шаг 2).

Шаг 3.

Время рисовать графические элементы. Я предпочитаю это делать полностью через код.

UILabel *valueLabel;

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

- (void) buildView {
    CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width;
    
    UIView *celsiusView = [[UIView alloc] initWithFrame:CGRectMake(
                                                                   0,
                                                                   0,
                                                                   screenWidth,
                                                                   200)];
    celsiusView.layer.borderColor = [UIColor grayColor].CGColor;
    celsiusView.layer.borderWidth = 1.0f;
    [self.view addSubview:celsiusView];
    
    UILabel *titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(
                                                                    30,
                                                                    30,
                                                                    celsiusView.frame.size.width - 60,
                                                                    20)];
    titleLabel.text = @"Weather in Prague";
    titleLabel.textAlignment = NSTextAlignmentCenter;
    [celsiusView addSubview:titleLabel];
    
    valueLabel = [[UILabel alloc] initWithFrame:CGRectMake(
                                                           30,
                                                           titleLabel.frame.origin.y + titleLabel.frame.size.height + 10,
                                                           celsiusView.frame.size.width - 60,
                                                           20)];
    valueLabel.text = @"---";
    valueLabel.textAlignment = NSTextAlignmentCenter;
    [celsiusView addSubview:valueLabel];
    
    UIButton *updateButton = [[UIButton alloc] initWithFrame:CGRectMake(
                                                                        (screenWidth - 200)/2,
                                                                        valueLabel.frame.origin.y + valueLabel.frame.size.height + 20,
                                                                        200,
                                                                        30)];
    [updateButton setTitle:@"Update" forState:UIControlStateNormal];
    updateButton.showsTouchWhenHighlighted = true;
    updateButton.backgroundColor = [UIColor grayColor];
    [updateButton addTarget:self action:@selector(updateWeather:) forControlEvents:UIControlEventTouchUpInside];
    [celsiusView addSubview:updateButton];
    
    
}

В конце вы должны получить подобный вид:

Screen Shot 2017-04-19 at 21.45.56

Шаг 4.

Не забудьте добавить “App Transport Security Settings” с элементом “Allow Arbitrary Loads” => “YES“ в info.plist, потому что мы собираемся делать небезопасный запрос (подключение к API без https).

Шаг 5.

Напишем логику. Пример кода:

- (void) updateWeather:(UIButton *) sender {
    
    //NSString *post = @"postKey=postVar";
    //NSData *postData = [post dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES];
    
    //NSString *postLength = [NSString stringWithFormat:@"%d", [postData length]];
    
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
    [request setURL:[NSURL URLWithString:@"http://api.openweathermap.org/data/2.5/weather?q=Praque&units=metric&APPID={YOUR_API_KEY}"]];
    [request setHTTPMethod:@"GET"];
    //[request setValue:postLength forHTTPHeaderField:@"Content-Length"];
    //[request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
    //[request setHTTPBody:postData];

    NSURLSession *session = [NSURLSession sharedSession];
    NSURLSessionDataTask *task = [session dataTaskWithRequest:request
                                            completionHandler:
                                  ^(NSData *data, NSURLResponse *response, NSError *error) {
                                      if (data == nil) {
                                          [self printCannotLoad];
                                      } else {
                                          [self parseWeatherJSON:data];
                                      }
                                  }];
    [task resume];

}

- (void) parseWeatherJSON:(NSData *) jsonData {
    NSError *error = nil;
    id object = [NSJSONSerialization
                 JSONObjectWithData:jsonData
                 options:0
                 error:&error];
        
    if(error) {
        [self printCannotLoad];
        return;
    }
        
    if ([object isKindOfClass:[NSDictionary class]]) {
        NSDictionary *mainObject = [object valueForKey:@"main"];
        NSDictionary *weatherObject = [object valueForKey:@"weather"][0];
        NSString *textString = [NSString stringWithFormat:@"%@, %@ celsius", weatherObject[@"description"], mainObject[@"temp"]];
        dispatch_sync(dispatch_get_main_queue(), ^{
            valueLabel.text = textString;
        });
    } else {
    }
}

- (void) printCannotLoad {
    dispatch_sync(dispatch_get_main_queue(), ^{
        valueLabel.text = @"cannot load";
    });
}

В функции updateWeather мы делаем HTTP запрос используя GET метод. Ключи и значения GE-а записаны напрямую в URL. В случае успеха получения данных мы запускаем parseWeatherJSON. Иначе показываем сообщение об ошибке (printCannotLoad function). Я закомментировал строки с примером POST запроса. Пожалуйста, обратите внимание что большинство примеров в интернете показывают старый, неактуальный способ с использованием NSURLConnection.

В parseWeatherJSON мы отделяем элементы «main» и «weather» из корнегого объекта. Обратите внимание, что мы запускаем редактирование вида в главном потоке. parseWeatherJSON запускается в бэкграунд потоке потому как эта функция запущена через completionHandler.

Шаг 6.

Теперь вы в любое время можете узнать погоду в Праге :)

Screen Shot 2017-04-22 at 00.35.00

Код этого проекта вы можете посмотреть в моем GitHub аккаунте.

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

Картинка профиля Doszhan Kalibek

Doszhan Kalibek

  • Igor Rublev

    приветствую.

    в общем сделал в своем приложение запрос таким же образом(изначально было почти также, небольшое отличие и все), но у меня постоянно при запросе данных(если выключен WiFi на девайсе, и включено LTE) — вываливается дурацкая ошибка, связанная с трассировкой пакетов, с чем я еще не сталкивался на работе, но уже прошерстил интернет на предмет решения проблемы.

    текст ошибки:

    NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9800).

    Task . HTTP load failed (error code: -1200 [3:-9800]).

    Task . finished with error — code: -1200.

    Возможно ли, что причина в самой API с которой я пытаюсь получить данные?

    https://forums.developer.apple.com/message/293708#293708 — тут пишут что проблема с трассировкой пакетов, но как исправить все равно не понятно.

  • Igor Rublev

    Вы у себя проверяли, работает ли вообще через LTE данным способом?

    • Хмм, странно.

      > Возможно ли, что причина в самой API с которой я пытаюсь получить данные?
      Для проверки данной догадки можно потестить другие API:
      https://github.com/toddmotto/public-apis

      > Вы у себя проверяли, работает ли вообще через LTE данным способом?
      Точно не помню, запускал ли через LTE.

      • Igor Rublev

        Вот сейчас затестил пример из данной статьи, все работает:)

      • Igor Rublev

        попробую в этот пример свою апиху подсунуть, но не думаю что изменится что-то