3

I'm creating a xml parser class with TBXML. I'd like the class to load an xml doc, traverse through it, and return an array of strings to populate a table. This should take place in a background thread so it does block the UI. I'd like to add a completion block so the table's data source array is set when the xml parsing is complete.

How do I implement the completion block? Here's what I have so far:

Parser.m

- (NSMutableArray *)loadObjects
{
    // Create a success block to be called when the asyn request completes
    TBXMLSuccessBlock successBlock = ^(TBXML *tbxmlDocument) {
        NSLog(@"PROCESSING ASYNC CALLBACK");

        // If TBXML found a root node, process element and iterate all children
        if (tbxmlDocument.rootXMLElement)
            [self traverseElement:tbxmlDocument.rootXMLElement];
    };

    // Create a failure block that gets called if something goes wrong
    TBXMLFailureBlock failureBlock = ^(TBXML *tbxmlDocument, NSError * error) {
        NSLog(@"Error! %@ %@", [error localizedDescription], [error userInfo]);
    };

    // Initialize TBXML with the URL of an XML doc. TBXML asynchronously loads and parses the file.
    tbxml = [[TBXML alloc] initWithURL:[NSURL URLWithString:@"XML_DOC_URL"]
                               success:successBlock
                               failure:failureBlock];
    return self.array;
}


- (void)traverseElement:(TBXMLElement *)element
{    
    do {
        // if the element has child elements, process them
        if (element->firstChild) [self traverseElement:element->firstChild];

        if ([[TBXML elementName:element] isEqualToString:@"item"]) {
            TBXMLElement *title = [TBXML childElementNamed:@"title" parentElement:element];
            NSString *titleString = [TBXML textForElement:title];
            [self.array addObject:titleString];
        };

        // Obtain next sibling element
    } while ((element = element->nextSibling));
}

TableViewController.m

- (void)viewDidLoad
{
    [super viewDidLoad];

    Parser *parser = [[Parser alloc] init];
    self.products = [parser loadObjects]; 
}
mnort9
  • 1,810
  • 3
  • 30
  • 54

1 Answers1

2

Like this, you'll get an empty array, right? That's because loadObjects is not blocking and returns before it could complete its task.

Now, you want your viewDidLoad to return immediately so your table can display. So in your TableViewController, you want a callback that gets called and manages the update after your empty table is displayed. The standard technique is to use delegation. In Parser.h define

@protocol ParserProtocol <NSObject>
-(void)parserDidFinishLoading;
@end

in the Parser-interface add

@property id<ParserProtocol> delegate;

Finish your successBlock with the line

[self.delegate parserDidFinishLoading];

then make TableViewController conforming to ParserProtocol and add the Parser as a property:

@property Parser* parser;

in TableViewController.m replace

Parser *parser = [[Parser alloc] init];
self.products = [parser loadObjects]; 

with

self.parser = [[Parser alloc] init];
self.products = [self.parser loadObjects]; 

and add

-(void)parserDidFinishLoading{
    [self.tableView reloadData];
}

The self.products points to the now modified array property of Parser. So no additional setup should be needed.

Good luck, Peter.

ilmiacs
  • 2,566
  • 15
  • 20
  • Thank you, this is very helpful. Only two things: 1)loadObjects is still returning an empty array, so instead I simply set `self.products = self.parser.array` property rather than the returned array. 2) I'm getting some really strange latency in between `self.tableview reloadData` and `cellForRow...`. It's causing my table to not be updated until like 15 seconds after parserDidFinishLoading is called. – mnort9 Oct 06 '12 at 22:42
  • I figured out the delay. `reloadData` was called in the background thread while in the delegate method. I had to replace `reloadData` with `[[self tableView] performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO];` – mnort9 Oct 06 '12 at 23:05
  • Is it ok to set a property to another class's property `self.products = self.parser.array`? – mnort9 Oct 08 '12 at 00:30
  • Yes, setting properties like this is perfectly ok. `self.parser.array` calls the getter of `array` and that returns a pointer. `self.products =` calls the setter of `products` with that pointer as argument. – ilmiacs Oct 08 '12 at 07:31
  • Yeah, I didn't notice the potential issue with the threads, good point. But I probably would call `performSelectorOnMainThread:` in the successBlock, e.g. with `parserDidFinishLoading` of the `delegate`. That way, the responsibility of switching threads is that of the `Parser` object alone. The ViewController should not know about other threads. – ilmiacs Oct 08 '12 at 07:42