SlideShare a Scribd company logo
Troubles start at
version 1
Laurent Cerveau
lcerveau@nephorider.com
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
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
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?”
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
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
It’s all about versioning
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
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;
}
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
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;
/* 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;
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
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
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"];
Data structure
•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
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
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
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
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...
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
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;
}
}
}
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...)
ThankYou!

More Related Content

Paris Tech Meetup talk : Troubles start at version 1.0

  • 1. Troubles start at version 1 Laurent Cerveau lcerveau@nephorider.com
  • 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"];
  • 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
  • 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...)