Paris Tech Meetup talk : Troubles start at version 1.0
Upcoming SlideShare
Loading in...5

Like this? Share it with your network


Paris Tech Meetup talk : Troubles start at version 1.0


  • 625 vues

Presentation given at the Paris Tech Meetup 2013. A checklist (with code example for iOS development) to anticipate potential problems that can happen during the upgrade process of an application.

Presentation given at the Paris Tech Meetup 2013. A checklist (with code example for iOS development) to anticipate potential problems that can happen during the upgrade process of an application.



Total des vues
Vues sur SlideShare
Vues externes



0 Ajouts 0

No embeds



Détails de l'import

Uploaded via as Adobe PDF

Droits d'utilisation

© Tous droits réservés

Report content

Signalé comme inapproprié Signaler comme inapproprié
Signaler comme inapproprié

Indiquez la raison pour laquelle vous avez signalé cette présentation comme n'étant pas appropriée.

  • Full Name Full Name Comment goes here.
    Êtes-vous sûr de vouloir
    Votre message apparaîtra ici
Poster un commentaire
Modifier votre commentaire

Paris Tech Meetup talk : Troubles start at version 1.0 Presentation Transcript

  • 1. Troubles start at version 1 Laurent Cerveau
  • 2. Before version 1.0 “You’ll never gonna die you gonna make it if you try they gonna love you” Focus is naturally on this first version, get feature working and debugged
  • 3. After version 1.0 “And in the end the love you take is equal to the love you make” Some future changes may break what is already exist and _really_ annoy users
  • 4. So the topic is... • I am shipping a new version of a client , and its internal have changed. • I am deploying new server code I do not want to have application crash or behave weirdly • I need to ship a new client... in preparation for future server changes • I would like that users upgrade to a new version • I need to force users to upgrade to the latest version Make an often heard question more useful “Which version is it?”
  • 5. Configuration Code snippets and practice advices to manage evolution of versions on client and server Client Native application Server API, push notification 1.0 1.0.1 1.1 2.0 v1 v2 v3 v4
  • 6. 2 main topics • How to overuse versioning capabilities • Structure tips for safe client server exchange No complicated stuff, many little things to ease future developments
  • 7. It’s all about versioning
  • 8. Apple system •Avoid going out of those conventions (1.2b) CFBundleVersion “Int” e.g. 34 Development CFBundleShortVersion String “String” e.g.1.2.3 Marketing •Apple environment provides 2 versions • Use agvtool to modify them (in a build system) agvtool bump -all agvtool new-marketing-version “1.0.1” (For each build that is distributed) • Here, each component stays below 10
  • 9. In Code • Set up the XCode project •Translate marketing version easily in integer for easy manipulation int MMUtilityConvertMarketingVersionTo3Digit(NSString *version) { NSMutableArray *componentArray = [NSMutableArray arrayWithArray:[version componentsSeparatedByString:@"."]]; for(NSUInteger idx = [componentArray count]; idx < 3; idx++) { [componentArray addObject:@0]; } __block int result = 0; [componentArray enumerateObjectsUsingBlock:^(NSString *aComponent, NSUInteger idx, BOOL *stop) { result = 10*result+[aComponent intValue]; }]; return result; }
  • 10. Centralized management MMEnvironmentMonitor singleton or on the application delegate, created early @property @methods firstTime, firstTimeForCurrentVersion, previousVersion applicationVariant developmentStage Tutorial of the app, present new features When you have a lite and a full version alpha, beta, final -(void) runUpgradeScenario -(void) detectBoundariesVersions(EndBlock) -(BOOL) testRunability Set up defaults, upgrade internal data structure Time limit for beta, warn if wrong OS version Read those parameters from the server
  • 11. Actions • Detect first launch(es) _current3DigitVersion = MMUtilityConvertMarketingVersionTo3Digit([[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]]); id tmpObj = [[NSUserDefaults standardUserDefaults] objectForKey:kNEPVersionRunDefaultsKey]; _firstTime = (nil == tmpObj); _firstTimeForCurrentVersion = (nil == [tmpObj objectForKey:[NSString stringWithFormat:@"%d",_current3DigitVersion]]); if(_firstTimeForCurrentVersion) { _previous3DigitVersion = [[[tmpObj keysSortedByValueUsingSelector:@selector(compare:)] lastObject] intValue]; [[NSUserDefaults standardUserDefaults] setObject:@{[NSString stringWithFormat:@"%d",_current3DigitVersion]:@YES} forKey:kNEPVersionRunDefaultsKey]; [[NSUserDefaults standardUserDefaults] synchronize]; } else { _previous3DigitVersion = _current3DigitVersion; } • Run Upgrade scenarios : convention naming /* Have all start and upgrade method named with the same scheme */ - (void) _startAt100; - (void) _startAt101; - (void) _upgradeFrom100To101; - (void) _upgradeFrom102To110;
  • 12. /* Use the Objective-C runtime */ - (BOOL) runUpgradeScenario { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" __block BOOL result = NO; if(NO == self.firstTimeForCurrentVersion && NO == self.firstTime) return result; } • Run Upgrade scenarios : apply the upgrade or start NSMutableDictionary *allUpgrades= [NSMutableDictionary dictionary]; NSMutableDictionary *allStarts= [NSMutableDictionary dictionary]; //Find all upgrade methods unsigned int outCount; Method * allMethods = class_copyMethodList([self class], &outCount); for(unsigned int idx = 0; idx < outCount; idx++) { Method aMethod = allMethods[idx]; NSString *aMethodName = NSStringFromSelector(method_getName(aMethod)); if([aMethodName hasPrefix:@"_upgradeFrom"]) { NSString *upgradeVersionString = [aMethodName substringWithRange:NSMakeRange([@"_upgradeFrom" length], 3)]; [allUpgrades setObject:aMethodName forKey:upgradeVersionString]; } else if ([aMethodName hasPrefix:@"_startAt"]) { NSString *startVersionString = [aMethodName substringWithRange:NSMakeRange([@"_startAt" length], 3)]; [allStarts setObject:aMethodName forKey:startVersionString]; } } if(allMethods) free(allMethods); if(self.firstTime) { //sort them and perform the most "recent" one SEL startSelector = NSSelectorFromString([allStarts[[[allStarts keysSortedByValueUsingSelector:@selector(compare:)]lastObject]]]); [self performSelector:startSelector withObject:nil]; result = YES; } else if(self.firstTimeForCurrentVersion) { //Sort them and apply the one that needs to be applied [[allUpgrades keysSortedByValueUsingSelector:@selector(compare:)] enumerateObjectsUsingBlock:^(NSString *obj, NSUInteger idx, BOOL *stop) { if([obj intValue] > _previous3DigitVersion) { result = YES; [self performSelector:NSSelectorFromString([allUpgrades objectForKey:obj]) withObject:nil]; } }]; } #pragma clang diagnostic pop return result;
  • 13. Runability • Beta lock : generate a .m file with limit date at each build. Put it in a Run script phase tmp_path = os.path.join(os.getcwd(),'Sources/frontend/NEPDevelopmentStage.m') tmp_now = time.strftime('%Y-%m-%d %X +0000', time.localtime(time.time() +24*3600*20)) f.write('NSString *const kMMLimitTimeForNonFinalVersion =@"') f.write(tmp_now) f.write('";') f.close() • Be nice and say goodbye
  • 14. Server can help • Have a server call returns information { last_version_in_apple_store:”1.2.3”, minimal_client_version:”1.0.1” } Run limited
  • 15. 2 Small server/client tips • It is always good to deal with HTTP 503 (service unavailable) instead of nothing happening. Useful for big (or non mastered) changes • Client limitation can be done with extra HTTP header or user agent change (be cautious) and send back 403 /* Change user agent */ userAgent = [NSString stringWithFormat:@"MyApp/%@ iOS CFNetwork/%@ Darwin/%s", [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"], cfNetworkVersion, kernelVersion]; [request setValue:userAgent forHTTPHeaderField:@"User-Agent"];
  • 16. Data structure
  • 17. •Whatever I send you, you should not crash on data (Obj-C nil insertion, receiving HTML instead of JSON...) •The user is not aware of what is not visible - spread the changes •The user may forgive missing data (if she/he has been prepared) •The user should always find back its environment Rules of thumb
  • 18. Consequences Self-describing uuid { __class_name:person, uuid:person_123456, firstname:laurent, lastname:cerveau, age:... } { __class_name:person, uuid:person_123456, data_version:1, object_version:2 firstname:laurent... } or
  • 19. The topic of uuid • use directly “id” of DB table unique only in one table, dangerous in case of DB technology change • use full uuidgen “B238BC15-DF27-4538-9FDA-2F972FE24B59” • use something helpful in debugging “video_12345” • use something with a meaning (FQDN) “video.tv_episode.12345” NB: server may forget UUID in case of non persistent data
  • 20. Base object @interface MMBaseObject : NSObject { NSString *_uuid; int _objectVersion; int _dataVersion; MMObjectType _type; } • Every object created with server data derives from such • Parsing of data instantiates all objects, ...if understood • Each object is responsible to be defensive in its parsing • Base object class methods allows creation of a factory • Use of a enum-based type can be convenient
  • 21. Step 1 Registration • Each subclass register itself at load time /* Register towards to the base class */ + (void)load { [MMBaseObject registerClass:NSStringFromClass([self class]) forType:kMMObjectTypePerson JSONClassName:@"person"]; } /* Class registration: to be called by subclasses */ + (void) registerClass:(NSString *)className forType:(MMObjectType)type JSONClassName:(NSString *)jsonClassName { if(nil == _allObjectClasses) _allObjectClasses = [[NSMutableDictionary alloc] init]; if(nil == _jsonClassToObjectClass) _jsonClassToObjectClass = [[NSMutableDictionary alloc] init]; @autoreleasepool { [_allObjectClasses setObject:[NSNumber numberWithUnsignedInteger:type] forKey:className]; [_jsonClassToObjectClass setObject:className forKey:jsonClassName]; } } • Registration maintains the mapping • Class method on the Base class allows to retrieve class from JSON class name and so on...
  • 22. Step 2 Parsing • Starts on the base class /* Entry point for JSON parsing and MMObject instantiations */ + (void) createMMObjectsFromJSONResult:(id)jsonResult { _ParseAPIObjectWithExecutionBlock(jsonResult); return ; } /* Transform a Cocoa object JSON inspired representation into a real object */ void _ParseAPIObjectWithExecutionBlock(id inputObj) { if([inputObj isKindOfClass:[NSArray class]]) { [inputObj enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { _ParseAPIObjectWithExecutionBlock(obj); }]; } else if([inputObj isKindOfClass:[NSDictionary class]]) { NSDictionary *tmpDictionary = (NSDictionary *)inputObj; NSString *objectAPIType = tmpDictionary[@"__class_name"]; NSString *objectUUID = tmpDictionary[@"uuid"] ; if(objectUUID) { MMBaseObject *tmpObject = [_dataStore objectWithUUID :objectUUID]; if(tmpObject) { [tmpObject updateWithJSONContent:tmpDictionary]; } else { if(nil == objectAPIType) return; NSString *objectClass = [BOXBaseObject classNameForStringAPIType:objectAPIType]; if(nil == objectClass) return result; tmpObject = [[NSClassFromString(objectClass) alloc] initWithJSONContent:tmpDictionary]; [_dataStore addObject:tmpObject replace:NO]; } [tmpDictionary enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { if([obj isKindOfClass:[NSArray class]] || [obj isKindOfClass:[NSDictionary class]]) { _ParseAPIObjectWithExecutionBlock(obj,provider, task, block, objectUUID, key); } }]; } } } •And inside calls a recursive function
  • 23. Step 3 Object creation • Base class does the basics /* Designated initializer. Possible Variation:if uuid is nil one can be generated */ - (id)initWithUUID:(NSString *)uuid { self = [super init]; if(self) { NSString *tmpClassString = NSStringFromClass([self class]); self.uuid = uuid; self.type = [_allObjectClasses[tmpClassString] unsignedIntegerValue]; } return self; } /* JSON object initialization : first time */ - (id)initWithJSONContent:(NSDictionary *)contentObject { self = [super initWithUUID:contentObject[@"uuid"]]; ! if (self != nil) { ! ! [self updateWithJSONContent:contentObject]; } ! return self; } •And subclass are defensive /* JSON update */ - (void)updateWithJSONContent:(NSDictionary *)contentObject { if([contentObject[@”object_version”] intValue] > _objectVersion) { id tmpObj = JSONContent[@"title"]; if(tmpObj && [tmpObj isKindOfClass:[NSString class]]) { self.title = tmpObj; } } }
  • 24. Down the road • Deal with a field based API (incomplete download) • Deal with sliced data • Gather all objects created in a HTTP call • Handle relationship between objects •Apply in an external framework (e.g. RestKit...)
  • 25. ThankYou!