1

Let's say I have a class Car, with the following property scheme:

@interface Car : NSObject

@property int carID;
@property Driver *driver;
@property NSString *brandName;
...

@end

Now, I have a dictionary of Car instances, and whenever I download a new instance from my server, I overwrite with carDictionary[@(newCar.carID)] = newCar; to keep my instances updated. The only issue is that sometimes my instances are downloaded with some properties not filled out.

When I download a new instance, is there a way to only overwrite using the non-nil properties I download and leave the nil properties alone? Obviously, I could manually set each property, but I don't want to have to change this every time I add a new property.

danielmhanover
  • 3,094
  • 4
  • 35
  • 51

3 Answers3

2

There's no built-in way in Objective-C to do this. That said, I think this is how I would approach it.

I would start by adding a method on Car which updated it with the new information:

-(void)updateWithNewCarInfo(Car *)newCar

The straightforward way is to just manually copy the properties yourself. But if you want to do a bit of future-proofing, you can use Objective-C's introspection capabilities to get a list of the properties on your class and iterate through them yourself. This question has some pointers on how to do that. You'll need to implement your own logic as to which properties to overwrite when, though.

Community
  • 1
  • 1
dpassage
  • 5,423
  • 3
  • 25
  • 53
  • I don't think he needs introspection, though, does he? Wouldn't key-value coding be sufficient? – matt Dec 23 '14 at 18:16
  • I don't think key-value coding will get you a list of available properties, will it? – dpassage Dec 23 '14 at 18:18
  • Well, he said he wants to future-proof: 'Obviously, I could manually set each property, but I don't want to have to change this every time I add a new property.' – dpassage Dec 23 '14 at 18:19
  • Agreed, I see your point there. Obviously if we have a hard-coded property name list, it would not be magically future-proof. – matt Dec 23 '14 at 18:21
  • The idea of using the objective-c runtime had occurred to me, but I wanted to make sure it was the only future-proof way before I used it. – danielmhanover Dec 23 '14 at 18:26
  • 2
    How about overriding `setNilValueForKey` to not update that key? – Abizern Dec 23 '14 at 19:06
  • 1
    My solution is already mucking about with the Obj-C runtime. Also overriding parts of KVO seems like tempting fate. :) – dpassage Dec 23 '14 at 20:02
2

Since there is already a Car object in carDictionary at this spot, instead of wantonly replacing it, write a Car method updateWithValuesFromCar: that takes another Car and does what you want done, and call that.

I would start with an array of property names @["carID", "driver", "brandName"] (and so on) and cycle through that, using key-value coding to check each incoming property value and decide whether to replace my own corresponding property value.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • This is the same notion as dpassage's answer. I didn't mean to "steal" his idea; we created our answers at exactly the same instant. Sorry about that. Just proves it's a good idea! – matt Dec 23 '14 at 18:14
1

Let's say you get a dictionary as a result of your web service call. Now you want to use that dictionary to instantiate or update a Car object, where an update does not overwrite existing valid data with nil data from the dictionary.

Assume the property names in your class correspond to the key names in your dictionary.

Here's an approach:

  1. Get the existing Car object for the current dictionary from the carDictionary:

    Car *currentCar = carDictionary[@(newCar.carID)];

  2. If no car is returned, instantiate a new one:

      if (currentCar == nil) {
        currentCar = [[Car alloc] init];
       }
    
  3. Assign all the values from your downloaded dictionary to the car instance:

    [currentCar setValuesForKeysWithDictionary:yourDownloadedDictionary];

    That method will map the keys from the dictionary with the keys in your Car instance. The dictionary keys need to match the keys in your class, and often it's worth just using the same names so you get to use this convenient method.

    It will throw an exception if there is a new key added to your web service dictionary that does not correlate to a key/property in your class. This would break your app if your web service started giving out new keys while some users had outdated versions of your app. To solve that..

  4. Override - (void)setValue:(id)value forUndefinedKey:(NSString *)key; so it does nothing, other than perhaps log out the fact that a key was attempted to be set but your class doesn't implement that key/property.

Then you have the problem of "Don't set nil values". You can tell your class not to overwrite existing content with nil values by...

  1. Override -(void)setValue:forKey:

    -(void)setValue:(id)value forKey:(NSString *)key {
       if (value != nil) {
         [super setValue:value forKey:key];
    } else {
             NSLog(@"Not changing %@ because its value is nil.", key);
       }
      }
    

It's a bit more work in setting up the class, but it gives you future proofing for when your web service expands its returned attribute set.

Woodster
  • 3,451
  • 3
  • 17
  • 19
  • Ah, yes. This is the answer – danielmhanover Dec 23 '14 at 20:18
  • Let's say, someone attacks your service and puts some keys in it, you do not expect, i. e. "class". Let's say the web service is updated and has a new key, but you have users that still run an old version? – Amin Negm-Awad Dec 23 '14 at 20:39
  • A sanity check could be run beforehand on the dictionary, to trim any extra (malicious) keys. Any ideas how to do that besides the obvious `allKeys` loop? – danielmhanover Dec 23 '14 at 23:05
  • If there are keys in the web service that are not KVC compliant, they will be ignored because of the overridden `forUndefinedKey` method. NSObject in iOS8.1 only has two declared properties, both of which are readonly, so you wouldn't be able to change them with malicious server code. @AminNegm-Awad, do you have an example of a value that could be set with the method you describe that would be detrimental to the integrity of the class? – Woodster Dec 24 '14 at 02:27
  • Yes, simply change a key, let's say "owner" is changed to "owners" (with an array) in the next version. Even if "owners" is ignored, owner is not set. (And then it is put in an owners collection $anywhere in your software …) This may harm the rest of the software. At the end of the day, the whole software cannot rely on anything being set. This causes a mass of extra checks. Generic solutions as in the answer are very sexy, because they demonstrate and use the power of Objctive-C. But one should never use it on network or file system. Do such things NSCoding style not KVC style. – Amin Negm-Awad Dec 24 '14 at 10:14
  • @sddhhanover Doing checks on which keys has to be there and other, that doesn't have to be there will lead you to more code compared to a non-generic NSCoding style deserialization. Especially when you have to do version handling or simply the fact, that the existence of a key depends of another key (or the value stored with another key) makes that check code more complex than a NSCoding style solution by far. – Amin Negm-Awad Dec 24 '14 at 10:18
  • When you use this approach, you don't "simply change a key" in future versions, you add keys. The previous keys are still sent from the service, which are used by older versions. Newer keys are added. Changing the data type from X to Y is adding a new key. Also typical in this approach is including an API version identifier in the URL request parameter (i.e.: blah.com/request/obj/API2) so if the schema changes significantly, it's the server that adapts the return values. For man-in-middle attacks that might change key values, use pinned SSL certificates. – Woodster Dec 24 '14 at 14:55