Pull to refresh

Работа с Vkontakte.ru API в iOS приложении

Reading time 12 min
Views 49K
В этой небольшой статье я хотел бы поделиться способами интеграции Вконтакте API в проект IOS.
По этой теме сейчас в интернете мало информации и возможно мои советы кому-то помогут. Я покажу как можно не только получить данные пользователя, но и разместить на стене фотографию, текст и ссылку, а так же случай с вводом капчи.

Проект с примером лежит на github.com – https://github.com/maiorov/VKAPI

Документацию по методам API ВКонтакте можно почитать тут

Статьи из других источников на эту тему:
1. appleinsider.ru
2. imaladec.net

Итак, несколько этапов в работе с VK API:
1. Авторизация пользователя через UIWebView и получение accessToken'a и идентификатора пользователя.
2. Использование VK API, запросы и получение данных:
2.1 Отправка текста на стену пользователя
2.1.1 Captcha
2.2 Отправка изображения на стену
2.2.1 Получение url сервера ВКонтакте для загрузги изображения
2.2.2 Формируем POST запрос
2.2.3 Создаем запрос на сохранение фото на сервере ВКонтакте
2.2.4 Размещаем фото на стене
3. Logout пользователя

Часть 0. Регистрация приложения
Как выяснилось, не самая тривиальная часть операции, из-за того, что ВКонтакте несколько форм создания mobile-приложений! Для правильной регистрации нужно воспользоваться ссылкой http://vkontakte.ru/editapp?act=create&site=1, выбрать тип приложения Standalone-приложение. Параметры в настройках можно не указывать. Нужно лишь скопировать ID приложения. В моем примере для этого параметра используется appID.

Часть 1. Авторизация
Для работы по протоколу Oauth 2.0 с VK API нам понадобится accessToken и user_id. Мы можем получить его отправив запрос на сервер VK через веб-браузер UIWebView, в ответ на запрос сервер ВКонтакте выдаст форму авторизации пользователя, после прохождения авторизации мы получим accessToken, user_id (id пользователя, который ввел свои данные), expires_in (время действия токена). Для этого в новом проекте создадим view-controller, назовем его например vkLoginViewController. Это будет контроллер отвечающий за авторизацию пользователя, при успешной авторизации, он сохраняет полученные параметры в NSUserDefaults и вызывает метод делегата authComplete.

Подробный код вы можете посмотреть в примере. Здесь я разберу только основные моменты.

Добавив в контроллер UIWebView *vkWebView, мы делаем запрос:
    NSString *authLink = [NSString stringWithFormat:@"http://api.vk.com/oauth/authorize?client_id=%@&scope=wall,photos&redirect_uri=http://api.vk.com/blank.html&display=touch&response_type=token", appID];
    NSURL *url = [NSURL URLWithString:authLink];
    
    [vkWebView loadRequest:[NSURLRequest requestWithURL:url]];

Тут client_id это id вашего приложения, scope=wall,photos – это права доступа, которые приложение запрашивает у пользователя (wall размещение на стене, photos нужно чтобы размещать изображения на стене)

В функции webViewDidFinishLoad получаем ответ от сервера ВКонтакте:
-(void)webViewDidFinishLoad:(UIWebView *)webView {
    // Если есть токен сохраняем его
    if ([vkWebView.request.URL.absoluteString rangeOfString:@"access_token"].location != NSNotFound) {
        NSString *accessToken = [self stringBetweenString:@"access_token=" 
                                                andString:@"&" 
                                              innerString:[[[webView request] URL] absoluteString]];
        
        // Получаем id пользователя, пригодится нам позднее
        NSArray *userAr = [[[[webView request] URL] absoluteString] componentsSeparatedByString:@"&user_id="];
        NSString *user_id = [userAr lastObject];
        NSLog(@"User id: %@", user_id);
        if(user_id){
            [[NSUserDefaults standardUserDefaults] setObject:user_id forKey:@"VKAccessUserId"];
        }
        
        if(accessToken){
            [[NSUserDefaults standardUserDefaults] setObject:accessToken forKey:@"VKAccessToken"];
            // Сохраняем дату получения токена. Параметр expires_in=86400 в ответе ВКонтакта, говорит сколько будет действовать токен.
            // В данном случае, это для примера, мы можем проверять позднее истек ли токен или нет
            [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:@"VKAccessTokenDate"];
            [[NSUserDefaults standardUserDefaults] synchronize];
        }
        
        NSLog(@"vkWebView response: %@",[[[webView request] URL] absoluteString]);
        [(ViewController *)delegate authComplete];
        [self dismissModalViewControllerAnimated:YES];
    } else if ([vkWebView.request.URL.absoluteString rangeOfString:@"error"].location != NSNotFound) {
        NSLog(@"Error: %@", vkWebView.request.URL.absoluteString);
        [self dismissModalViewControllerAnimated:YES];
    }
    
}


После этого, мы получили все необходимое: accessToken, user_id. И передаем делегату, что авторизация успешно пройдена, больше нам этот контроллер не нужен и мы его убираем:
[(ViewController *)delegate authComplete];
[self dismissModalViewControllerAnimated:YES];


Часть 2. Запросы к API
Теперь начинается самое интересное! В примере в контроллере ViewController я подготовил несколько функций, по которым вы сможете понять как работать с API.

2.1 Отправка текста на стену пользователя
Возьмем, для примера, функцию отправки текста на стену:
- (void) sendText {
    NSString *user_id = [[NSUserDefaults standardUserDefaults] objectForKey:@"VKAccessUserId"];
    NSString *accessToken = [[NSUserDefaults standardUserDefaults] objectForKey:@"VKAccessToken"];
    
    NSString *text = @"Работать с ВКонтакте API легко!";
    
    // Создаем запрос на размещение текста на стене
    NSString *sendTextMessage = [NSString stringWithFormat:@"https://api.vk.com/method/wall.post?owner_id=%@&access_token=%@&message=%@", user_id, accessToken, [self URLEncodedString:text]];
    NSLog(@"sendTextMessage: %@", sendTextMessage);
    
    // Если запрос более сложный мы можем работать дальше с полученным ответом
    NSDictionary *result = [self sendRequest:sendTextMessage withCaptcha:NO];
    // Если есть описание ошибки в ответе
    NSString *errorMsg = [[result objectForKey:@"error"] objectForKey:@"error_msg"];
    if(errorMsg) {
        [self sendFailedWithError:errorMsg];
    } else {
        [self sendSuccessWithMessage:@"Текст размещен на стене!"];
    }
}

Для простоты примера, я храню данные в NSUserDefaults. Авторизация пройдена, значит у нас есть все необходимое чтобы составить запрос. Используя метод wall.post, передаем id пользователя (owner_id) и токен (access_token), сообщение text для параметра message мы предварительно прогоняем через функцию URLEncodedString:
// Функция преобразования текста в подходящий для передачи формат
- (NSString *)URLEncodedString:(NSString *)str
{
    NSString *result = (NSString *)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault,
                                                                           (CFStringRef)str,
                                                                           NULL,
																		   CFSTR("!*'();:@&=+$,/?%#[]"),
                                                                           kCFStringEncodingUTF8);
    [result autorelease];
	return result;
}


После того, как мы подготовили запрос, нужно отправить его на сервер с помощью функции:
[self sendRequest:sendTextAndLinkMessage withCaptcha:NO];
Как вы можете заметить, фунцкция принимает параметр withCaptcha:(BOOL) это понадобится нам в случае, если сервер затребует ввод капчи у пользователя для подтверждения действий. Так как пока что капча не требуется, мы передаем NO.

Собственно, сама функция отправки запроса:
- (NSDictionary *) sendRequest:(NSString *)reqURl withCaptcha:(BOOL)captcha {
    // Если это запрос после ввода капчи, то добавляем в запрос параметры captcha_sid и captcha_key
    if(captcha == YES){
        NSString *captcha_sid = [[NSUserDefaults standardUserDefaults] objectForKey:@"captcha_sid"];
        NSString *captcha_user = [[NSUserDefaults standardUserDefaults] objectForKey:@"captcha_user"];
        // Добавляем к запросу данные для капчи. Не забываем, что введенный пользователем текст нужно обработать.
        reqURl = [reqURl stringByAppendingFormat:@"&captcha_sid=%@&captcha_key=%@", captcha_sid, [self URLEncodedString: captcha_user]];
    }
    NSLog(@"Sending request: %@", reqURl);
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:reqURl] 
                                                            cachePolicy:NSURLRequestReloadIgnoringLocalCacheData 
                                                        timeoutInterval:60.0]; 
    
    // Для простоты используется обычный запрос NSURLConnection, ответ сервера сохраняем в NSData
    NSData *responseData = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:nil];
    
    // Если ответ получен успешно, можем его посмотреть и заодно с помощью JSONKit получить NSDictionary
    if(responseData){
        NSDictionary *dict = [[JSONDecoder decoder] parseJSONData:responseData];
        
        // Если есть описание ошибки в ответе
        NSString *errorMsg = [[dict objectForKey:@"error"] objectForKey:@"error_msg"];
        
        NSLog(@"Server response: %@ \nError: %@", dict, errorMsg);
        
        // Если требуется ввод капчи. Тут я проверяю на строку Captcha needed, хотя лучше использовать код ошибки.
        if([errorMsg isEqualToString:@"Captcha needed"]){
            isCaptcha = YES;
            // Сохраняем параметры для капчи
            NSString *captcha_sid = [[dict objectForKey:@"error"] objectForKey:@"captcha_sid"];
            NSString *captcha_img = [[dict objectForKey:@"error"] objectForKey:@"captcha_img"];
            [[NSUserDefaults standardUserDefaults] setObject:captcha_img forKey:@"captcha_img"];
            [[NSUserDefaults standardUserDefaults] setObject:captcha_sid forKey:@"captcha_sid"];
            // Сохраняем url запроса чтобы сделать его повторно после ввода капчи
            [[NSUserDefaults standardUserDefaults] setObject:reqURl forKey:@"request"];
            [[NSUserDefaults standardUserDefaults] synchronize];
            
            [self getCaptcha];
        }
        
        return dict;
    }
    return nil;
}

Функция возвращает NSDictionary ответ сервера, для его формирования используется JSONKit, так как по-умолчанию ВКонтакте отвечает в формате JSON.

Captcha
Если вдруг сервер решил, что пользвателю нужно ввести капчу, то он, в ответ на запрос, вернет ошибку с текстом: Captcha needed и параметрами – captcha_sid (это id капчи), captcha_img (ссылка на изображение капчи). Вооружившись этими параметрами и заодно сохранив запрос, по которому сервер нам так ответил, мы вызываем функцию ввода капчи:
[self getCaptcha];

Рассмотрим ее чуть подробнее:
- (void) getCaptcha {

    NSString *captcha_img = [[NSUserDefaults standardUserDefaults] objectForKey:@"captcha_img"];
    UIAlertView *myAlertView = [[UIAlertView alloc] initWithTitle:@"Введите код:\n\n\n\n\n"
                                                          message:@"\n" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"OK", nil];
    
    UIImageView *imageView = [[[UIImageView alloc] initWithFrame:CGRectMake(12.0, 45.0, 130.0, 50.0)] autorelease];
    imageView.image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:captcha_img]]];
    [myAlertView addSubview:imageView];
    
    UITextField *myTextField = [[[UITextField alloc] initWithFrame:CGRectMake(12.0, 110.0, 260.0, 25.0)] autorelease];
    [myTextField setBackgroundColor:[UIColor whiteColor]];
    
    // Отключаем автокорректировку
    myTextField.autocorrectionType = UITextAutocorrectionTypeNo;
    // Отключаем автокапитализацию
    myTextField.autocapitalizationType = UITextAutocapitalizationTypeNone;
    myTextField.tag = 33;
    
    [myAlertView addSubview:myTextField];
    [myAlertView show];
    [myAlertView release];
}

- (void)alertView:(UIAlertView *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
{
    if(isCaptcha && buttonIndex == 1){
        isCaptcha = NO;
        
        UITextField *myTextField = (UITextField *)[actionSheet viewWithTag:33];
        [[NSUserDefaults standardUserDefaults] setObject:myTextField.text forKey:@"captcha_user"];
        NSLog(@"Captcha entered: %@",myTextField.text);
        
        // Вспоминаем какой был последний запрос и делаем его еще раз
        NSString *request = [[NSUserDefaults standardUserDefaults] objectForKey:@"request"];
        
        NSDictionary *newRequestDict =[self sendRequest:request withCaptcha:YES];
        NSString *errorMsg = [[newRequestDict  objectForKey:@"error"] objectForKey:@"error_msg"];
        if(errorMsg) {
            [self sendFailedWithError:errorMsg];
        } else {
            [self sendSuccessWithMessage:@"Капча обработана успешно и запрос выполнен!"];
        }
    }
}

Используя стандартный UIAlertView, с добавлением UIImageView и UITextField, и параметр captcha_img, мы даем пользователю возможность ввести капчу. После того как капча введена, в функции делегата UIAlertView сохраняем то, что ввел пользователь в NSUserDefaults с ключем captcha_user и используя ранее сохраненный url запроса, делаем его повторно, на этот раз указав в параметре withCaptcha: YES. В итоге, перед отправкой запроса, к нему добавляюся параметы captcha_sid и captcha_key (то что ввел пользователь):
- (NSDictionary *) sendRequest:(NSString *)reqURl withCaptcha:(BOOL)captcha {
    // Если это запрос после ввода капчи, то добавляем в запрос параметры captcha_sid и captcha_key
    if(captcha == YES){
        NSString *captcha_sid = [[NSUserDefaults standardUserDefaults] objectForKey:@"captcha_sid"];
        NSString *captcha_user = [[NSUserDefaults standardUserDefaults] objectForKey:@"captcha_user"];
        // Добавляем к запросу данные для капчи. Не забываем, что введенный пользователем текст нужно обработать.
        reqURl = [reqURl stringByAppendingFormat:@"&captcha_sid=%@&captcha_key=%@", captcha_sid, [self URLEncodedString: captcha_user]];
    }
...

Капча введена, запрос отправлен, текст должен быть успешно размещен на стене пользователя!

2.2 Отправка изображения на стену
Часто встречается необходимость разместить фото из приложения на стене пользователя. Чтобы решить эту задачу нужно пройти следующие этапы:
  1. Запрос url сервера ВКонтакте для загрузки нашего изображения (photos.getWallUploadServer)
  2. По полученной ссылке в ответе сервера отправляем изображение методом POST
  3. Получив в ответе hash, photo, server, отправлем команду на сохранение фото на стене (photos.saveWallPhoto)
  4. Получив в ответе photo id, делаем запрос на размещение на стене картинки с помощью wall.post, где в качестве attachment указываем photo id


2.2.1 Получение url сервера ВКонтакте для загрузки изображения
- (IBAction)sendImageAction:(id)sender {
    if(!isAuth) return;
    UIImage *image = [UIImage imageNamed:@"test.jpg"];
    NSString *user_id = [[NSUserDefaults standardUserDefaults] objectForKey:@"VKAccessUserId"];
    NSString *accessToken = [[NSUserDefaults standardUserDefaults] objectForKey:@"VKAccessToken"];
    
    // Этап 1 
       
    NSString *getWallUploadServer = [NSString stringWithFormat:@"https://api.vk.com/method/photos.getWallUploadServer?owner_id=%@&access_token=%@", user_id, accessToken];
    
    NSDictionary *uploadServer = [self sendRequest:getWallUploadServer withCaptcha:NO];
    
    // Получаем ссылку для загрузки изображения
    NSString *upload_url = [[uploadServer objectForKey:@"response"] objectForKey:@"upload_url"];

...

Теперь, получив ссылку для загрузки изображения, нам нужно сделать POST запрос с вложенным изображением. Для этого изображение преобразуем в NSData и создаем запрос с помощью функции
[self sendPOSTRequest:upload_url withImageData:imageData];


2.2.2 Формируем POST запрос
...
// Этап 2 
    // Преобразуем изображение в NSData
    NSData *imageData = UIImageJPEGRepresentation(image, 1.0f);
    
    NSDictionary *postDictionary = [self sendPOSTRequest:upload_url withImageData:imageData];
    
    // Из полученного ответа берем hash, photo, server 
    NSString *hash = [postDictionary objectForKey:@"hash"];
    NSString *photo = [postDictionary objectForKey:@"photo"];
    NSString *server = [postDictionary objectForKey:@"server"];
...

Функцию формирующую POST-запрос вы можете посмотреть в примере.

2.2.3 Создаем запрос на сохранение фото на сервере ВКонтакте
...
// Этап 3 
    // Создаем запрос на сохранение фото на сервере вконтакте, в ответ получим id фото
    NSString *saveWallPhoto = [NSString stringWithFormat:@"https://api.vk.com/method/photos.saveWallPhoto?owner_id=%@&access_token=%@&server=%@&photo=%@&hash=%@", user_id, accessToken,server,photo,hash];
    
    NSDictionary *saveWallPhotoDict = [self sendRequest:saveWallPhoto withCaptcha:NO];
    
    NSDictionary *photoDict = [[saveWallPhotoDict objectForKey:@"response"] lastObject];
    NSString *photoId = [photoDict objectForKey:@"id"];
...

В ответ получаем photo id загруженной фотографии, теперь осталось сделать обычный запрос wall.post, где в качестве параметра attachment указать photo id. Если нужно еще добавить ссылку, то достаточно после photo id, через запятую указать ее.

2.2.4 Размещаем фото на стене
...
// Этап 4 
    // Постим изображение на стену пользователя
     NSString *postToWallLink = [NSString stringWithFormat:@"https://api.vk.com/method/wall.post?owner_id=%@&access_token=%@&message=%@&attachment=%@", user_id, accessToken, [self URLEncodedString:@"К изображению можно добавить текст и ссылку"], photoId];
    
    NSDictionary *postToWallDict = [self sendRequest:postToWallLink withCaptcha:NO];
    NSString *errorMsg = [[postToWallDict  objectForKey:@"error"] objectForKey:@"error_msg"];
    if(errorMsg) {
        [self sendFailedWithError:errorMsg];
    } else {
        [self sendSuccessWithMessage:@"Картинка размещена на стене!"];
    }
    // Ура! Фото должно быть на стене!


Часть 3. Logout пользователя
Чтобы сделать logout достаточно отправить на сервер ВКонтакте запрос:
NSString *logout = @"http://api.vk.com/oauth/logout";

Пример функции logout:
- (IBAction)logout:(id)sender {
    // Запрос на logout
    NSString *logout = @"http://api.vk.com/oauth/logout";
    
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:logout] 
                                                            cachePolicy:NSURLRequestReloadIgnoringLocalCacheData 
                                                        timeoutInterval:60.0]; 
    NSData *responseData = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:nil];
    if(responseData){
        NSDictionary *dict = [[JSONDecoder decoder] parseJSONData:responseData];
        NSLog(@"Logout: %@", dict);
        
        // Приложение больше не авторизовано, можно удалить данные
        isAuth = NO;
        [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"VKAccessUserId"];
        [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"VKAccessToken"];
        [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"VKAccessTokenDate"];
        [[NSUserDefaults standardUserDefaults] synchronize];
        
        [self sendSuccessWithMessage:@"Выход произведен успешно!"]; 
    }
}


Несмотря на то, что ВКонтакте отвечает ошибкой, выход пользователя все-таки происходит :)
Logout: {
    error = "invalid_client";
    "error_description" = "client_id is incorrect";
}


Заключение
Надеюсь, что эта статья поможет вам в освоении API Вконтакте. Буду рад комментариям, дополнениям.
Tags:
Hubs:
+14
Comments 19
Comments Comments 19

Articles