Paris Tech Meetup talk : Troubles start at version 1.0
- 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
- 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...)