2

I have the following requirement:

Given a hierarchical tree-like structure, I am performing a breadth-first-search to walk through the WHOLE dataset. The data is being provided by an API with a method : (makes a request to a server using AFNetworking, saves the result to Core Data and calls back the completion block on success with the stored entries)

-(void) getChildrenForNodeId:(NSNumber*)nodeId completion:(void (^)(NSArray *nodes))completionBlock;

The method which a controller executes to fetch data:

 -(void)getAllNodesWithCompletion:(void (^)(NSArray *nodes))completionBlock{

     NSNumber *rootId = ...

     [MyNetworkManager getChildrenForNodeId:rootId completion:^(NSArray *nodes){

        for(Node *node in nodes){

             [self iterateOverNode:node.nodeId];

        }

       //execute completionBlock with nodes fetched from database that contain all their children until the very last leaf

     }];

  }

Here is the problem:

-(void)iterateOverNode:(NSNumber*)nodeId {

    NSMutableArray *elements = [NSMutableArray array];

    [elements addObject:nodeId];

    while ([elements count]) {

        NSNumber *current = [elements objectAtIndex:0];

        [MyNetworkManager getChildrenForNodeWithId:current completion:^(NSArray *nodes) {

              /**
                In order to continue with the loop the elements array must be updated. This can only happen once we have retrieved the children of the current node. 
However since this is in a loop, all of these requests will be sent off at the same time, thus unable to properly keep looping. 
          */
          for(Node *node in nodes){
              [elements addObject:node.nodeId];
          }

          [elements removeObjectAtIndex:0];

        }];

    }

}

Basically I need the result of the callback to control the flow of the while loop but I am not sure how to achieve it. My understanding is that the request to getChildrenForNodeWithId:completion: from within the while-loop should happen in a new thread in a SERIAL order so that another should commence after the first one has completed. I am not sure how to achieve this neither with NSOperation nor with GCD. Any help will be greatly appreciated.

Community
  • 1
  • 1
Petar
  • 2,241
  • 1
  • 24
  • 38
  • So are you asking how you would go about downloading all the nodes and saving them into core data, given that each node may contain other nodes? – Sam Clewlow Feb 20 '15 at 15:58
  • Exactly. I understand the problem, I cannot figure out how to do it with an async web service request. – Petar Feb 23 '15 at 08:43
  • What is the implementation of MyNetworkManager? – Sam Clewlow Feb 23 '15 at 14:52
  • Subclass of AFHTTPRequestOperationManager which calls the API for the given info. When data is retrieved it is being saved into core data. When the save is complete, the completion block is executed. – Petar Feb 23 '15 at 15:01
  • Did my solution help at all? – Sam Clewlow Feb 27 '15 at 11:44
  • I havent tried it yet, because we changed the API to provide flat responses. However it is looking good and I am looking forward to giving it a go. Many thanks ! – Petar Feb 27 '15 at 11:48

1 Answers1

1

What you need here is some recursion. This problem is tricky as we also need a way to track and detect the point at which we have explored every branch to a leaf node.

I'm not a expert with tree search algorithms, so some folks could probably improve on my answer here. Kick this off by calling it with the root node id. self.trackingArray is an NSMutableArray property with __block qualifier. Each time we start a request for a Node, we add it's nodeId into this array, and when it returns, we remove it's nodeId, and add the nodeIds of it's children. We can then know that when count of the tracking array reaches 0, every request made has returned, and has not added child ids to the array. Once you detect we are finished, you could call a saved block or a delegate method.

This solution does not include any error handling. If any request fails, it won't be retried, and all child nodes will be ignored.

- (void)getNodesRecursively:(NSNumber *)nodeId {

    // Only do this once
    if (self.trackingArray == nil) {
        self.trackingArray = [NSMutableArray new];
        [self.trackingArray addObject:nodeId];
    }
    __weak typeof(self) weakSelf = self;

    [MyNetworkManager getChildrenForNodeWithId:nodeId completion:^(NSArray *nodes) {

        [self.trackingArray removeObject:nodeId];

        for (Node *node in nodes) {

            [weakSelf.trackingArray addObject:node.nodeId];
            [weakSelf getNodesRecursively:node.nodeId];
        }

        if (weakSelf.trackingArray.count == 0) {
            // We are done.
            // Reset the state of the trackingArray
            self.trackingArray = nil;

            // call a method here to notify who ever needs to know.
            [weakSelf allNodesComplete];
        }

    }];
}

Your other methods would look something like this

-(void)getAllNodesWithCompletion:(void (^)(NSArray *nodes))completionBlock{

     // Save the block to a property 
     self.completion = completionBlock;

     // Call -getNodesRecursively with root id
     [self getNodesRecursively:rootNodeId];
}

Then you could have a second method

- (void)allNodesComplete {

      // Call saved block
      // Completion should load nodes from core data as needed
      self.completion();
}

I haven't tested the code, but does that approach seem sensible? I'm assuming we don't need to capture the here nodes, as they can be loaded from core data as required.

Sam Clewlow
  • 4,293
  • 26
  • 36