Возврат значения из асинхронного вызова с использованием семафоров

Мне нужно использовать 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

Замечания:

  1. Я знаю, что вы будете возражать против восстановления этого обратно в асинхронный метод, но это действительно плохая идея сделать это синхронным. Во-первых, это ужасный UX (приложение зависнет, и пользователь не будет знать, действительно ли оно что-то делает или оно мертво), и если вы находитесь в медленной сети, у вас могут возникнуть всевозможные проблемы (например, сторожевой процесс может убить ваше приложение, если вы делаете это в неподходящее время).

    Так что держите это асинхронным. В идеале показать UIActivityIndicatorView перед запуском асинхронного входа и выключите его в completionHandler, completionHandler также будет инициировать следующий шаг в процессе (например, performSegueWithIdentifier).

  2. Я не беспокоюсь о тестировании HTML-контента; легче просто попытаться разобрать JSON и посмотреть, успешно это или нет. Таким образом, вы также обнаружите более широкий спектр ошибок.

  3. Лично я не вернул бы свои собственные объекты ошибок. Я просто продолжил бы и возвращал объекты ошибок, которые ОС дала мне. Таким образом, если вызывающий должен был различать разные коды ошибок (например, нет соединения с ошибкой сервера), вы могли бы.

    И если вы используете свои собственные коды ошибок, я бы посоветовал не менять domain, domain должен охватывать целую категорию ошибок (например, один пользовательский домен для всех внутренних ошибок вашего приложения), а не изменяться от одной ошибки к другой. Не рекомендуется использовать domain поле для чего-то вроде сообщений об ошибках. Если вы хотите что-то более описательное в вашем NSError объект, поместите текст сообщения об ошибке в userInfo толковый словарь.

  4. Я мог бы предложить имена методов / переменных для соответствия соглашениям об именах Какао (например, классы начинаются с заглавной буквы, переменные и имена методов и параметры начинаются с строчной буквы).

  5. Там нет необходимости устанавливать 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;

См. Блоки и Переменные в Темах Программирования Блоков для получения дополнительной информации.

Другие вопросы по тегам