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.
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...
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...)