0

现在,我收到了一个 GET 请求,完成后,我得到了 json,然后我想使用 json 中的 id 来执行另一个 fetch 请求。这就像一个接一个的嵌套获取请求。例如:

+ (void)searchPhotoWithTags:(NSArray *)tags page:(NSInteger)page perPage:(NSInteger)perPage completionBlock:(void (^)(NSArray *, NSError *))block
{
    NSDictionary *dict = @{@"method": @"search", @"api_key": kAppKey, @"tags": [tags componentsJoinedByString:@","], @"per_page": [NSString stringWithFormat:@"%d", perPage], @"page": [NSString stringWithFormat:@"%d", page], @"format": @"json"};
    [[LDHttpClient sharedClient] getPath:@"" parameters:dict success:^(AFHTTPRequestOperation *operation, id responseObject) {      
        [[responseObject valueForKeyPath:@"photos.photo"] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
            Photo *photo = [[Photo alloc] initWithPhotoId:[NSNumber numberWithInt:[obj[@"id"] integerValue]] andSecret:obj[@"secret"]];
            //down below, I want to use photo.photoId to execute another request but the data is not completed. what's the better way to do this?
            [PhotoSize getPhotoSizesWithPhotoId:photo.photoId completionBlock:^(NSArray *photoSizes, NSError *error) {
                [photos addObject:@{@"photo": photo, @"sizes": photoSizes}];
            }];
        }];
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
    }];
}
4

3 回答 3

4

如果我正确理解了您的问题,我认为您所看到的是异步问题。

您正在尝试遍历您的照片字典,通过发送另一个异步操作的 GET 请求来获取每张照片的照片大小。但是,正因为如此,您的循环的下一次迭代已经在您之前的异步操作完成之前执行。

在这种情况下,您可以做的是使用递归来帮助您“迭代”或“循环”通过您的照片字典。

要求

要使以下代码正常工作,您需要创建 2 个属性

  • 用于在 yourClass.h 文件中存储“照片NSDictionary(例如 NSDictionary *photosDict)的属性
  • 另一个用于存储“照片”NSDictionary 枚举数的属性,其类型为NSEnumerator,可以称之为“ photosEnum

稍微清理一下你的代码

在您的原始方法中,存储照片字典,然后也存储 photosEnum 枚举器:

+ (void)searchPhotoWithTags:(NSArray *)tags page:(NSInteger)page perPage:(NSInteger)perPage completionBlock:(void (^)(NSArray *, NSError *))block
{
    NSDictionary *dict = @{@"method": @"search", @"api_key": kAppKey, @"tags": [tags componentsJoinedByString:@","], @"per_page": [NSString stringWithFormat:@"%d", perPage], @"page": [NSString stringWithFormat:@"%d", page], @"format": @"json"};

    [[LDHttpClient sharedClient] getPath:@"" parameters:dict success:^(AFHTTPRequestOperation *operation, id responseObject) {      

        // I assume you have a property of type NSDictionary created called "photos"
        self.photosDict = [responseObject valueForKeyPath:@"photos"];

        // Also create a property for the enumerator of type NSEnumerator
        self.photosEnum = [self.photosDict objectEnumerator];

        // ----------------------------------------------------------
        // First call of our recursion method
        //
        // This will start our "looping" of our photos enumerator
        // -----------------------------------------------------------
        [self processPhotoDictionary];

    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        NSLog(@"Failed to get photos, error: %@", [error localizedDescription]);
    }];
}

最后,我们的递归方法对 photoSizes 进行了处理:

-(void)processPhotoDictionary
{
    // ------------------------------------------------------
    // Because self.photosEnum is a property of our class
    // it remembers where it is "up to" in the "looping"
    // ------------------------------------------------------
    NSDictionary *photo = [self.photosEnum nextObject];

    if(photo != nil)
    {
        Photo *photoObj = [[Photo alloc] initWithPhotoId:[NSNumber numberWithInt:[[photo valueForKey:@"id"] integerValue]] 
                                            andSecret:[photo valueForKey:@"secret"]];

        [PhotoSize getPhotoSizesWithPhotoId:photoObj.photoId completionBlock:^(NSArray *photoSizes, NSError *error) {
            [photos addObject:@{@"photo": photoObj, @"sizes": photoSizes}];

            // ------------------------------------------------------
            // Here we're using recursion to iterate through our
            // enumerator due to asynchronous nature instead of the
            // while loop.
            // ------------------------------------------------------
            [self processPhotoDictionary];
        }];
    }
}

希望有帮助。

于 2013-08-14T07:02:25.297 回答
2

除了@Zhang 的出色回答,我想描述一下 OP 面临的常见问题,以及这个常见问题的“通用解决方案”可能是什么样子。

共同的目标是:

  1. 从服务器获取项目列表。每个项目都包含一个指向其他资源(例如图像)的 URL。

  2. 收到列表后,对于列表中的每个项目,获取 URL 给出的资源(图像)。

当以同步方式实现这一点时,解决方案很明显,实际上也很容易。然而,当采用异步风格时——这是进行网络时的首选方式——一个可行的解决方案变得异常复杂,除非你知道如何解决这些问题;)

这里有趣的部分是#2。第 1 部分可以通过异步调用和完成函数简单地完成,其中完成函数调用第 2 部分。

为了使事情更容易理解,我将做一些简化和一些先决条件:

  1. 在第 #1 部分中,我们获得了一个元素列表,比如一个NSArray包含我们元素的对象。每个元素都有一个属性,它是另一个资源的 URL。

    现在,我们可以轻松地假设我们已经一个表示 N 个输入值的元素数组,这些元素将在一个循环中异步处理 - 一个接一个。让我们将该数组命名为“源数组”。

  2. 我们将处理异步方法/函数。让方法/函数发出信号表明它已完成异步处理的一种方法是完成处理程序(一个块)。

    所有完成处理程序的通用签名将定义如下:

    typedef void (^completion_t)(id result);

    注:result代表异步函数或方法的最终结果。它可能是我们期望的那种东西(例如图像),或者它可能指示错误,例如通过传递和NSError对象。

  3. 为了实现我们的第 2 部分,我们需要一个异步方法/函数,它接受一个输入(来自输入数组的一个元素)并产生一个输出。这对应于您的“获取图像资源”任务。稍后我们需要为我们在第 1 部分中获得的“输入数组”的每个元素应用此方法/函数。

    通用函数,一个“转换函数”,将具有以下签名:

    void transform(id input, completion_t completion);

    相应的方法将具有此签名:

    -(void) transformWithInput:(id)input 
                    completion:(completion_t)completionHandler;
    

    我们可以为函数定义一个 typedef,如下所示:

    typedef void (^transform_t)(id input, completion_t completion);
    

请注意,转换函数或方法的结果将通过完成处理程序的参数传递。同步函数只有一个返回值并返回结果。

注意:名称“transform”只是一个通用名称。您可以将您的网络请求包装在一个方法中并获得这种“转换”功能。在 OP 的示例中,URL将是输入参数,而完成处理程序的结果参数将是从服务器获取的图像(或错误)。

注意:这个和下面的简化只是为了让异步模式的解释更容易理解。在实践中,异步函数或方法可能需要其他输入参数,并且完成处理程序也可能具有其他参数。


现在,更“棘手”的部分:

以异步方式实现循环

嗯,这与同步编程风格有点“不同”。

有目的地,我们定义了某种forEach函数或方法来执行此迭代。该函数或方法本身就是异步的!我们现在知道任何异步函数或方法都会有一个完成处理程序。

因此,如果是函数,我们可以如下声明“forEach”函数:

`void transform_each(NSArray* inArray, transform_t task, completion_t completion);`

transform_each 依次将异步变换函数任务应用于输入数组inArray中的每个对象。处理完所有输入后,它会调用完成处理程序completion

完成处理程序的结果参数是一个数组,其中包含每个变换函数的结果,其顺序与相应输入的顺序相同。

注意:这里的“顺序”意味着输入一个接一个地处理。该模式的一个变体可以并行处理输入。

参数inArray是我们从第 1 步收集的“输入数组”。

参数任务是我们的异步转换函数,它几乎可以是任何接受输入并产生输出的东西。这将是我们在 OP 示例中的异步“获取图像”任务。

参数完成是在处理完所有输入后调用的处理程序。它的参数包含数组中每个变换函数的输出。

transform_each可以如下实现。首先我们需要一个“助手”功能do_each

do_each实际上是以异步方式实现循环的整个模式的核心,所以你可以在这里仔细看看:

void do_each(NSEnumerator* iter, transform_t task, NSMutableArray* outArray, completion_t completion)
{
    id obj = [iter nextObject];
    if (obj == nil) {
        if (completion)
            completion([outArray copy]);
        return;
    }
    task(obj, ^(id result){
        [outArray addObject:result];
        do_each(iter, task, outArray, completion);
    });
}

这里有趣的部分,以及用于实现循环(作为 for_each 函数)的“通用异步模式”或“惯用语”do_each将从转换函数的完成处理程序中调用。这可能看起来像递归,但实际上并非如此。

参数iter指向数组中要处理的当前对象。它还将用于确定停止条件:当枚举器指向结束时,我们nil从 method 获得结果nextObject。这最终会停止循环。

否则,将以当前对象作为输入参数调用变换函数任务。该对象将按照任务的定义进行异步处理。完成后,将调用任务的完成处理程序。它的参数结果将是变换函数的输出。处理程序需要将结果添加到结果数组 outArray中。然后它do_each再次调用助手。这似乎是一个递归调用,但实际上不是:前者do_each已经被返回。这只是另一个调用do_each.

一旦我们有了它,我们就可以简单地完成我们的transform_each功能,如下所示:

void transform_each(NSArray* inArray, transform_t task, completion_t completion) {
    NSMutableArray* outArray = [[NSMutableArray alloc] initWithCapacity:[inArray count]];
    NSEnumerator* iter = [inArray objectEnumerator];
    do_each(iter, task, outArray, completion);
}

NSArray 类别

为了方便起见,我们可以使用“forEach”方法轻松地为 NSArray 创建一个类别,该方法按顺序异步处理输入:

@interface NSArray (AsyncExtension)
- (void) async_forEachApplyTask:(transform_t) task completion:(completion_t) completion;
@end

@implementation NSArray (AsyncExtension)
- (void) async_forEachApplyTask:(transform_t) task completion:(completion_t) completion {
    transform_each(self, task, completion);
}
@end

可以在 Gist 上找到代码示例:transform_each

解决常见异步模式的一个更复杂的概念是利用“Futures”或“Promises”。我已经在一个小型库中为 Objective-C 实现了“承诺”的概念:RXPromise

上面的“循环”可以实现,包括通过 RXPromise取消异步任务的能力,当然还有更多。玩得开心 ;)

于 2013-08-14T09:01:51.607 回答
0

我想我可能会解决这个问题。我不确定。它只是工作。我正在使用 AFNetwroking 的 enqueueBatchOfHTTPRequestOperations 函数。

于 2013-08-19T01:33:15.173 回答