Возврат значения из асинхронного вызова с использованием семафоров
Мне нужно использовать NSURLSession
совершать сетевые звонки. На основании определенных вещей, после получения ответа, мне нужно вернуть NSError
объект.
Я использую семафоры, чтобы асинхронный вызов вел себя синхронно. Проблема в том, что ошибка установлена правильно в вызове, но как только семафор заканчивается
dispatch_semaphore_wait(семафор, DISPATCH_TIME_FOREVER);
), err
становится ноль.
Пожалуйста помоги
Код:
-(NSError*)loginWithEmail:(NSString*)email Password:(NSString*)password
{
NSError __block *err = NULL;
// preparing the URL of login
NSURL *Url = [NSURL URLWithString:urlString];
NSData *PostData = [Post dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
// preparing the request object
NSMutableURLRequest *Request = [[NSMutableURLRequest alloc] init];
[Request setURL:Url];
[Request setHTTPMethod:@"POST"];
[Request setValue:postLength forHTTPHeaderField:@"Content-Length"];
[Request setHTTPBody:PostData];
NSMutableDictionary __block *parsedData = NULL; // holds the data after it is parsed
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.TLSMinimumSupportedProtocol = kTLSProtocol11;
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:nil];
NSURLSessionDataTask *task = [session dataTaskWithRequest:Request completionHandler:^(NSData *data, NSURLResponse *response1, NSError *err){
if(!data)
{
err = [NSError errorWithDomain:@"Connection Timeout" code:200 userInfo:nil];
}
else
{
NSString *formattedData = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@", formattedData);
if([formattedData rangeOfString:@"<!DOCTYPE"].location != NSNotFound || [formattedData rangeOfString:@"<html"].location != NSNotFound)
{
loginSuccessful = NO;
//*errorr = [NSError errorWithDomain:@"Server Issue" code:201 userInfo:nil];
err = [NSError errorWithDomain:@"Server Issue" code:201 userInfo:nil];
}
else
{
parsedData = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&err];
NSMutableDictionary *dict = [parsedData objectForKey:@"User"];
loginSuccessful = YES;
}
dispatch_semaphore_signal(semaphore);
}];
[task resume];
// but have the thread wait until the task is done
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return err;
}
3 ответа
Я бы предложил разрубить узел Гордиана: вам не следует использовать семафоры, чтобы асинхронный метод вел себя синхронно. Примите асинхронные шаблоны, например, используйте обработчик завершения:
- (void)loginWithEmail:(NSString *)email password:(NSString*)password completionHandler:(void (^ __nonnull)(NSDictionary *userDictionary, NSError *error))completionHandler
{
NSString *post = ...; // build your `post` here, making sure to percent-escape userid and password if this is x-www-form-urlencoded request
NSURL *url = [NSURL URLWithString:urlString];
NSData *postData = [post dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:@"POST"];
// [request setValue:postLength forHTTPHeaderField:@"Content-Length"]; // not needed to set length ... this is done for you
[request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"]; // but it is best practice to set the `Content-Type`; use whatever `Content-Type` appropriate for your request
[request setValue:@"text/json" forHTTPHeaderField:@"Accept"]; // and it's also best practice to also inform server of what sort of response you'll accept
[request setHTTPBody:postData];
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.TLSMinimumSupportedProtocol = kTLSProtocol11;
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:nil];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *err) {
if (!data) {
dispatch_async(dispatch_get_main_queue(), ^{
completionHandler(nil, [NSError errorWithDomain:@"Connection Timeout" code:200 userInfo:nil]);
});
} else {
NSError *parseError;
NSDictionary *parsedData = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&parseError];
dispatch_async(dispatch_get_main_queue(), ^{
if (parsedData) {
NSDictionary *dict = parsedData[@"User"];
completionHandler(dict, nil);
} else {
completionHandler(nil, [NSError errorWithDomain:@"Server Issue" code:201 userInfo:nil]);
}
});
}
}];
[task resume];
}
А потом назовите это так:
[self loginWithEmail:userid password:password completionHandler:^(NSDictionary *userDictionary, NSError *error) {
if (error) {
// do whatever you want on error here
} else {
// successful, use `userDictionary` here
}
}];
// but don't do anything reliant on successful login here; put it inside the block above
Замечания:
Я знаю, что вы будете возражать против восстановления этого обратно в асинхронный метод, но это действительно плохая идея сделать это синхронным. Во-первых, это ужасный UX (приложение зависнет, и пользователь не будет знать, действительно ли оно что-то делает или оно мертво), и если вы находитесь в медленной сети, у вас могут возникнуть всевозможные проблемы (например, сторожевой процесс может убить ваше приложение, если вы делаете это в неподходящее время).
Так что держите это асинхронным. В идеале показать
UIActivityIndicatorView
перед запуском асинхронного входа и выключите его вcompletionHandler
,completionHandler
также будет инициировать следующий шаг в процессе (например,performSegueWithIdentifier
).Я не беспокоюсь о тестировании HTML-контента; легче просто попытаться разобрать JSON и посмотреть, успешно это или нет. Таким образом, вы также обнаружите более широкий спектр ошибок.
Лично я не вернул бы свои собственные объекты ошибок. Я просто продолжил бы и возвращал объекты ошибок, которые ОС дала мне. Таким образом, если вызывающий должен был различать разные коды ошибок (например, нет соединения с ошибкой сервера), вы могли бы.
И если вы используете свои собственные коды ошибок, я бы посоветовал не менять
domain
,domain
должен охватывать целую категорию ошибок (например, один пользовательский домен для всех внутренних ошибок вашего приложения), а не изменяться от одной ошибки к другой. Не рекомендуется использоватьdomain
поле для чего-то вроде сообщений об ошибках. Если вы хотите что-то более описательное в вашемNSError
объект, поместите текст сообщения об ошибке вuserInfo
толковый словарь.Я мог бы предложить имена методов / переменных для соответствия соглашениям об именах Какао (например, классы начинаются с заглавной буквы, переменные и имена методов и параметры начинаются с строчной буквы).
Там нет необходимости устанавливать
Content-Length
(это сделано для вас), но это хорошая практика, чтобы установитьContent-Type
а такжеAccept
(хотя и не обязательно).
Ответ Роба говорит вам, как сделать это правильно, но не какую ошибку вы сделали:
У вас есть две переменные с именем err, которые абсолютно не связаны. Кажется, что вы не включили некоторые важные предупреждения, иначе ваш код даже не скомпилировался бы.
Параметр err, который передается в ваш блок завершения, является ошибкой из запроса URL. Вы заменили его, не задумываясь, ошибкой тайм-аута, поэтому истинная ошибка теперь потеряна. Учтите, что тайм-аут не единственная ошибка.
Но все ошибки, которые вы устанавливаете, устанавливают только локальную переменную err, которая была передана вам в блоке завершения; они никогда не касаются переменной err в вызывающей программе.
PS. Несколько серьезных ошибок в вашей обработке JSON. JSON может быть в UTF-16 или UTF-32, и в этом случае formattedData будет иметь значение nil, и вы неправильно напечатаете "Проблема с сервером". Если данные не являются JSON, нет гарантии, что они содержат DOCTYPE или html, этот тест является абсолютным мусором. Ваш пользователь с ником JoeSmith будет вас ненавидеть.
Передача NSJSONReadingAllowFragments в NSJSONSerialization - нонсенс. dict не является изменчивым; если вы попытаетесь изменить его, ваше приложение упадет. Вы не проверяете, что анализатор возвратил словарь, вы не проверяете, есть ли значение для ключа "Пользователь", и вы не проверяете, что это словарь. Это может привести к краху вашего приложения.
Вы должны сообщить компилятору, что вы будете изменять err
, Требуется особая обработка, чтобы сохранить это после срока службы блока. Объявите это с __block
:
__block NSError *err = NULL;
См. Блоки и Переменные в Темах Программирования Блоков для получения дополнительной информации.