0

I'm trying to parse through an XML I'm getting from a webservice with an NSXMLParserDelegate. The NSObject I want to put the data into (I'll just call it MyObject) has @propertys that are not NSStrings (there are some NSDates, some NSNumbers, and maybe at some point in the future some primitives).

Since the XML is all a big string, I'm getting errors and crashes when I try to just directly put the values from the XML into my properties (I'm using [myObject setValue:currentElementValue forKey:elementName]; in my parser:didEndElement:..., which normally works fine - when key is a NSString). I didn't really expect this to work, but I figured it was worth a shot.

The easiest thing to do is just hardcode all of it, so if I have an '@property (nonatomic, strong) NSNumber *age;' on my object, when I find the <age>25</age> part of my XML, I do MyObject.age = [NSNumber numberWithInt:[currentElementValue intValue]];. The problem with this is it is very rigid - I'd like this code to be more dynamic than that. I don't want to have to know what all the properties of my object are ahead of time.

I've also tried checking what the class of the property is, then doing some conversion before entering a value (so something like [myObject setValue:[NSNumber numberWithInt:[currentElementValue intValue] forKey:elementName]; if the property isKindOfClass:[NSNumber class]]. The problem with this is that this is the first time that property is used, so it is nil, and therefore doesn't have a class. So this approach doesn't really work either.

Am I doing this completely wrong? It seems like it should be fairly typical to get data from an XML and put it into non-NSString variables, but I can't seem to get it to work.

If anyone needs to see more code to get a better understanding of how I've this is set up or what I've already tried, let me know.


EDIT: I've come up with a quick example of what I'm trying to do. Suppose I have the following XML:

<Object>
    <ITEM_NUMBER>4</ITEM_NUMBER>
    <IS_AWESOME>YES</IS_AWESOME>
    <AWESOMENESS>9000.1</AWESOMENESS>
</Object>

that I want to parse into an instance of MyObject (an NSObject subclass)

@interface MyObject: NSObject

@property (nonatomic, assign) int ITEM_NUMBER;
@property (nonatomic, assign) BOOL IS_AWESOME;
@property (nonatomic, assign) float AWESOMENESS;

@end

And a second XML

<OtherObject>
    <PRICE>4.99</PRICE>
    <QUANTITY>5</QUANTITY>
    <FOR_RESALE>NO</FOR_RESALE>
</OtherObject>

that will go into an instance of OtherObject (also NSObject subclass)

@interface OtherObject: NSObject

@property (nonatomic, assign) int QUANTITY;
@property (nonatomic, assign) BOOL FOR_RESALE;
@property (nonatomic, assign) float PRICE;

@end

I want to create a "template" parser (which has @property (nonatomic, strong) NSObject *instance; and then NSMutableString *currentElementValue as an ivar)

...
- (void) parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
{
    [currentElementValue setString:string];
}

- (void) parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName
{
    if ([[self.instance valueForKey:elementName] isKindOfClass:int])
    {
        [self.instance setValue:[currentElementValue intValue] forKey:elementName];
    }
    else if ([[self.instance valueForKey:elementName] isKindOfClass:float])
    {
        [self.instance setValue:[currentElementValue floatValue] forKey:elementName];
    }
    else if ([[self.instance valueForKey:elementName] isKindOfClass:BOOL])
    {
        [self.instance setValue:[currentElementValue boolValue] forKey:elementName];
    }
    else if ...
}
...

Then, I just subclass that "template" parser for each of my different XMLs and overwrite the init method so that

self.instance = [[MyObject alloc] init];

or

self.instance = [[OtherObject alloc] init];

as appropriate. I have a lot of webservices I'm calling that I will be getting similarly structured XML from, so only having to change 1 line in the parsers for each is something that is very desirable. Having all the rest of the parser code in a single "template" class will make maintaining, reading, and debugging the code so much easier.

Obviously, the problem I'm having is isKindOfClass doesn't accept int, float, or BOOL as inputs. Is there anything similar that I can use instead?

GeneralMike
  • 2,951
  • 3
  • 28
  • 56
  • 1
    It's common for the parser delegate to know about the properties of your objects. There's nothing wrong with creating the `NSNumber` object from the XML value and assigning it to your object. However, if you wanted to make your code a little more reusable, you could create a loader for each of your model classes, to avoid having all your code in the `parser:didEndElement:` method. Then just pass your attributes to each loader. – Craig Otis Nov 07 '13 at 18:02
  • @CraigOtis: If I do the "loader" idea you suggested, I'm going to need to essentially write a custom setter for every property in my objects right? So really I still have to do the same amount of work as hardcoding it in the `didEndElement:` method, it just moves where that work is done, correct? – GeneralMike Nov 07 '13 at 19:49
  • Your properties should all have setters anyway. You should *not* rely on the `setValue:forKey:` methods to create and customize your objects. – Craig Otis Nov 07 '13 at 19:59
  • @CraigOtis: Well the way I understand it, the setters are automatically created for all `@property`s. I've never had to create/overwrite one myself. When I create the object that hold the data I read from the XML, I make sure I have a property that matches each element in the XML. That's what makes `setValue:forKey:` work, and it was my impression that was the best-practice way of setting it up. I'm also creating the XML that is sent by the webservice, so will will always know what to expect ahead of time - but I suppose if that wasn't the case it could cause some trouble. – GeneralMike Nov 07 '13 at 20:19
  • I would avoid `setValue:forKey:` unless you have a very specific reason to use it. The setters *are* automatically created for you if you're using properties, and you should be using those to customize your objects. Just use `myObject.age = ...` in your loader class for whatever type `myObject` is. – Craig Otis Nov 07 '13 at 20:37

2 Answers2

1

The problem with this is that this is the first time that property is used, so it is nil, and therefore doesn't have a class.

Objective-C allows to query class properties at runtime, so introspecting the property type and using an appropriate trnsformation is one way to go.

You can read more about runtime at Mike Ash's blog or see Objective C Introspection/Reflection.

However, this specific problem has been encountered by many other people, so there are quite a few solutions: as said in iOS Most mature REST service library RESTKit is one good example of such a library; MagicRecord is another.

You can easily find a couple more libraries to check out before you decide to implement your own solution.

Community
  • 1
  • 1
ilya n.
  • 18,398
  • 15
  • 71
  • 89
  • I've already got most of the parsing and supporting stuff figured out, so I think the introspection thing sounds like what will be easiest for me at this point. I read over the links you provided, but I didn't see anything that actually described how to tell the variable type for a given `property`, which is where I'm stuck right now. Do you know the syntax for doing that? – GeneralMike Nov 18 '13 at 20:18
  • 1
    Ah, that does require some clicking through. It's `property_getAttributes(property)`.Look at https://developer.apple.com/library/mac/documentation/cocoa/conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html or play with https://github.com/mikeash/MAObjCRuntime/blob/master/RTProperty.m – ilya n. Nov 18 '13 at 20:26
  • Just a note: you need to `#import ` to use it! – GeneralMike Nov 22 '13 at 16:19
0

Of course I could just override the init method of MyObject to alloc-init all my properties to empty values, which should solve the nil problem I referenced in my OP, but that just feels really hacky. It may be the only option though.


EDIT: this approach totally falls apart with the example in the EDIT in the OP. KVC will turn ints, floats, and BOOLs all to NSNumbers, without any way of knowing what they originally where. Which means I won't be able to call [currentElementValue intValue], [currentElementValue floatValue], or [currentElementValue boolValue] as appropriate.

GeneralMike
  • 2,951
  • 3
  • 28
  • 56