Search code examples
iphoneiosobjective-cxmlnsxmlparser

Saving XML Parsed data to NSMutableDictionary Best Practice


This question might have been asked frequently but I have read almost all of them and yet couldn't figure out a solution for my case. I plan to save the parsed data to a NSMutableDictionary. Parser works ok and If I get a log it shows all data parsed. the problem is the last Item only saves in the NSDictionary. I can guess that I placed the dictionary in a wrong method but I can't figure out a better solution for saving. I use dictionary in order to hold the name and the text of element. here is my code :

@implementation Parser
@synthesize currentElementPointer, rootElement;

@synthesize dictionary;

-(id)initParser
{
    if(self = [super init]) {
        tvc = (TimeTableViewController*)[[UIApplication sharedApplication]delegate];

        APUAppDelegate *appDelegate = [[UIApplication sharedApplication]delegate];
        context = [appDelegate managedObjectContext];

        [self deleteAllObjects:@"TimeTable"];
    }
    return self;
}


#pragma mark -
#pragma mark PARSER

-(void)parserDidStartDocument:(NSXMLParser *)parser
{
    self.parsing = YES;
}

-(void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict
{
    if([elementName isEqualToString:@"intake"])
    {
        NSString *name = attributeDict[@"name"];
        self.parsing = [name isEqualToString:[self sendIntakeToParser]];
    }
    if(![self isParsing]) return;

    if(self.rootElement == nil) {
        self.rootElement = [[List alloc]init];
        self.currentElementPointer = self.rootElement;
    } else {
        List *newList = [[List alloc]init];
        newList.parent = self.currentElementPointer;
        [self.currentElementPointer.subElements addObject:newList];
        self.currentElementPointer = newList;
    }

    self.currentElementPointer.name = elementName;
    self.currentElementPointer.attributes = attributeDict;

}

-(void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
{

    if(![self isParsing]) return;

    if([self.currentElementPointer.text length] > 0) {
        self.currentElementPointer.text = [self.currentElementPointer.text stringByAppendingString:string];
    } else {
        self.currentElementPointer.text = string;
    }


    dictionary = [[NSMutableDictionary alloc]init];
    [dictionary setObject:[self.currentElementPointer text] forKey:[self.currentElementPointer name]];

}


-(void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName
{
    if([self isParsing])
    {
        self.currentElementPointer = self.currentElementPointer.parent;
    } else if ([elementName isEqualToString:@"intake"])
    {
        self.parsing = YES;
    }
}

This is my xml structure: enter image description here


Solution

  • The source of your problem is that you re-instantiate your dictionary with each call to parser:foundCharacters: which will be called at least once per element that has text. One thing you could do is move the dictionary instantiation to parser:didStartElement:... whenever elementName is equal to @"timetable", instantiate a mutable array in parser:didStartDocument: and then, in parser:didEndElement:..., if the elementName is `@"timetable", add it to your array.

    An example of this is shown below:

    Parser.h:

    #import <Foundation/Foundation.h>
    
    @interface Parser : NSObject <NSXMLParserDelegate>
    
    - (void)parseData:(NSData *)data;
    
    @end
    

    Parser.m:

    #import "Parser.h"
    
    @interface Parser ()
    
    @property (nonatomic,strong) NSMutableArray *timetableDictionaries;
    @property (nonatomic,strong) NSMutableDictionary *currentTimetableDictionary;
    @property (nonatomic,copy) NSString *currentElementName;
    
    @end
    
    @implementation Parser
    
    - (void)parseData:(NSData *)data
    {
        NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data];
        parser.delegate = self;
        [parser parse];
    }
    
    
    - (void)parserDidStartDocument:(NSXMLParser *)parser
    {
        self.timetableDictionaries = [NSMutableArray array];
    }
    
    
    - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict
    {
        self.currentElementName = elementName;
        if ([elementName isEqualToString:@"timetable"]) {
            self.currentTimetableDictionary = [NSMutableDictionary dictionary];
        }
    }
    
    
    - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
    {
        if (self.currentElementName) {
            NSString *existingCharacters = self.currentTimetableDictionary[self.currentElementName] ? : @"";
            NSString *nextCharacters = [existingCharacters stringByAppendingString:[string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]];
            if ([nextCharacters length] > 0) {
                self.currentTimetableDictionary[self.currentElementName] = nextCharacters;
            }
        }
    }
    
    
    - (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName
    {
        if ([elementName isEqualToString:@"timetable"]) {
            [self.timetableDictionaries addObject:self.currentTimetableDictionary];
            self.currentTimetableDictionary = nil;
        }
        self.currentElementName = nil;
    }
    
    
    - (void)parserDidEndDocument:(NSXMLParser *)parser
    {
        NSLog(@"timetable dictionaries = %@",self.timetableDictionaries);
    }
    
    @end
    

    I tested the above class with the following file:

    <weekof week="2013-07-29">
        <intake name="APCF1304">
            <timetable>
                <date>SOME DATE</date>
                <time>SOME TIME</time>
                <lecturer>SOME LECTURER</lecturer>
            </timetable>
            <timetable>
                <date>ANOTHER DATE</date>
                <time>ANOTHER TIME</time>
                <lecturer>ANOTHER LECTURER</lecturer>
            </timetable>
        </intake>
    </weekof>
    

    And it gave the following output:

    2013-08-11 11:50:03.205 MyParser[25368:c07] timetable dictionaries = (
            {
            date = "SOME DATE";
            lecturer = "SOME LECTURER";
            time = "SOME TIME";
        },
            {
            date = "ANOTHER DATE";
            lecturer = "ANOTHER LECTURER";
            time = "ANOTHER TIME";
        }
    )
    

    However, you've built a tree of the document already, presumably for another purpose, so you could just re-use that at the end of your parsing to build your dictionaries. For example, if you just wanted dictionaries for each timetable, you could do something that looked like this, in pseudocode:

    dictionaries = [new mutable array]
    element = self.root
    [extract dictionaries from: element to:dictionaries]
    
    implementation of extractDictionariesFrom:element to:dictionaries:
        if any subelement has non-nil text:
            item = [new mutable dictionary]
            for each sub element:
                item[element name] = element text
            [add item to dictionaries]
        else
            for each subelement:
                [extract dictionaries from: subelement to: dictionaries]
    

    Then you could discard the use of the mutable dictionary in your parser callback.