Beliebte Suchanfragen

Cloud Native

DevOps

IT-Security

Agile Methoden

Java

//

Handling app updates in iOS/macOS apps

14.12.2017 | 7 minutes of reading time

TL;DR: A solution is shown in which every app update step (one step per app version) is wrapped in its own class implementing a special AppUpdate protocol. On app launch, all classes implementing this protocol are loaded via reflection, instantiated, sorted by version number ascending and then each update step is executed if it hasn’t been already executed earlier.

One of the problems mobile engineers have to face is regular app updates. This problem is generally not new. However, we decided to present our solution to this problem because it may offer some interesting points for some.

Prior to the update, an app can have its state persisted in many ways and updating the app often requires transforming the existing data or creating new data. Two major problems exist in this scenario:

  1. we need to make sure update steps are executed even when a user skipped one or more app versions (ie user was on wild offline vacation, and in the meantime, two new app updates got released)
  2. we need to make sure update steps are executed only once (if update steps were executed every time user starts an app, it would be redundant and very likely break things)

The code demonstrating this approach is available on Bitbucket for both Swift and ObjC . Let’s quickly go through the major steps.

In the begining

Usually, we want application updates to be executed as soon as the app starts. The usual callback for that is didFinishLaunchingWithOptions in AppDelegate. So, in there we would add a line which does the app updating:

1[AppUpdater performUpdateSteps];
1AppUpdater.performUpdateSteps()

Simple as that. Also, for debugging reasons, we add the following in AppDelegate, to print the simulator app’s data folder in a console:

1#if TARGET_IPHONE_SIMULATOR
2    NSLog(@"Documents Directory: %@", [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]);
3#endif
1#if (arch(i386) || arch(x86_64)) && os(iOS)
2    let dirs: [String] = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory,
3            FileManager.SearchPathDomainMask.allDomainsMask, true)
4    print(dirs[dirs.endIndex - 1])
5#endif

That way, we can easily track the Library/Preferences folder where the Preferences plist file is kept and delete it for easier manual testing of update steps.

AppUpdater

Now let’s look into what’s going on in AppUpdater since it does the majority of work.

Guided by SRP , we have a class for every version update. Therefore, executing app updates comes down to iterating through update class instances and executing its update step. An example is given in the code bellow:

1ASTEach(updaters, ^(id <AppUpdate> updater) {
2    if ([updater canExecuteUpdate])
3    {
4        [self performUpdateStepWithUpdater:updater];
5    }
6});
1for updater in updaters {
2    if updater.canExecuteUpdate() {
3        performUpdateStepWithUpdater(updater)
4    }
5}

In case you’re wondering where the ASTEach in the ObjC example comes from, it’s from Asterism , a functional toolbelt for Objective-C.
Pretty simple. Bellow is the class diagram showing relations between AppUpdater and AppUpdate protocol which is implemented by specific updaters (v1.0.0, v1.1.0, etc.)

Each update step has its own unique identifier (more on that later). To make sure app updates execute only once, update step identifiers can be stored in UserDefaults. Before executing an update, the app would check if the update identifier already exists in UserDefaults, and only execute the update step if it doesn’t exist. All this gives us the code for an AppUpdater class:

1@implementation AppUpdater
2 
3+ (void) performUpdateSteps
4{
5    NSArray* updaters = @[
6        [AppUpdate_001_000_000 new],
7        [AppUpdate_001_001_000 new]
8    ];
9 
10    updaters = ASTSort(updaters, ^NSComparisonResult(id <AppUpdate> obj1, id <AppUpdate> obj2) {
11        return [obj1.version compare:obj2.version];
12    });
13 
14    ASTEach(updaters, ^(id <AppUpdate> updater) {
15        if ([updater canExecuteUpdate])
16        {
17            [self performUpdateStepWithUpdater:updater];
18        }
19    });
20}
21 
22+ (void) performUpdateStepWithUpdater:(id <AppUpdate> const)updater
23{
24    if (![NSUserDefaults.standardUserDefaults objectForKey:updater.version])
25    {
26        NSLog(@"▸ Performing update step: %@", updater.stepName);
27        updater.updateBlock();
28        [NSUserDefaults.standardUserDefaults setValue:updater.stepDescription forKey:updater.version];
29        NSLog(@"▸ Finished update step: %@", updater.stepName);
30        [NSUserDefaults.standardUserDefaults synchronize];
31    }
32}
33 
34@end
1class AppUpdater {
2 
3    class func performUpdateSteps() {
4        let updaters = [
5            AppUpdate_001_000_000(),
6            AppUpdate_001_001_000()
7        ]
8 
9        for updater in updaters {
10            if updater.canExecuteUpdate() {
11                performUpdateStepWithUpdater(updater)
12            }
13        }
14        UserDefaults.standard.synchronize()
15    }
16 
17    class func performUpdateStepWithUpdater(_ updater: AppUpdate) {
18        if (UserDefaults.standard.object(forKey: updater.stepName()) == nil) {
19            print("▸ Performing update step \(updater.stepName())")
20            updater.updateBlock()()
21            UserDefaults.standard.setValue(updater.stepDescription(), forKey: updater.stepName())
22            print("▸ Finished update step: \(updater.stepName())")
23        }
24    }
25}

The method performUpdateStepWithUpdater executes an app update step if it determines (by looking up UserDefaults) that it hasn’t been already executed. App update instances are listed in the updaters array sorted in the order in which they should be executed. Update classes are conveniently named like AppUpdate_001_001_000 where the ‘001_001_000’ suffix describes the app version like 1.1.0, supporting versions up to 999.999.999. This makes it useful for sorting app update classes in the IDE project explorer tree, but it also paves the way for another approach: instead of listing all updater classes in the array like we did:

1NSArray* updaters = @[
2    [AppUpdate_001_000_000 new],
3    [AppUpdate_001_001_000 new]
4];
1let updaters = [
2    AppUpdate_001_000_000(),
3    AppUpdate_001_001_000()
4]

we can retrieve all classes implementing the AppUpdate protocol by reflection , so our performUpdateSteps method comes down to:

1+ (void) performUpdateSteps
2{
3    NSArray<Class>* updateClasses = [Reflection classesImplementingProtocol:@protocol(AppUpdate)];
4    NSArray* updaters = ASTMap(updateClasses, (id (^)(id)) ^id(Class updateClass) {
5        return [updateClass new];
6    });
7 
8    updaters = ASTSort(updaters, ^NSComparisonResult(id <AppUpdate> obj1, id <AppUpdate> obj2) {
9        return [obj1.version compare:obj2.version];
10    });
11 
12    ASTEach(updaters, ^(id <AppUpdate> updater) {
13        if ([updater canExecuteUpdate])
14        {
15            [self performUpdateStepWithUpdater:updater];
16        }
17    });
18}
1class func performUpdateSteps() {
2    let updateClasses = getClassesImplementingProtocol(p: AppUpdate.self)
3    let updaters = updateClasses.map({ (updaterClass) -> AppUpdate in
4        return updaterClass.alloc() as! AppUpdate
5    }).sorted {
6        $0.version() < $1.version()
7    }
8 
9    for updater in updaters {
10        if updater.canExecuteUpdate() {
11            performUpdateStepWithUpdater(updater)
12        }
13    }
14    UserDefaults.standard.synchronize()
15}

Once classes are extracted and instantiated, they are sorted by app version to ensure proper order of update step execution. Extracting all classes implementing a given protocol happens in getClassesImplementingProtocol method in Reflection.swift or Reflection.h. The content of those files can be looked up on bitbucket .
As an effect, all the developer has to do in order to execute the new app update is to create a class implementing the AppUpdate protocol.

AppUpdate step

Every app update step has its own class implementing the AppUpdate protocol. An example implementation is given bellow:

1@implementation AppUpdate_01_001_00
2 
3- (NSString* const) version
4{
5    return @"001-001-000";
6}
7 
8- (BOOL const) canExecuteUpdate
9{
10    return SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(9);
11}
12 
13- (NSString* const) stepName
14{
15    return @"SpotlightIndexing";
16}
17 
18- (NSString* const) stepDescription
19{
20    return @"Index documents and photos in spotlight.";
21}
22 
23- (void (^)(void)) updateBlock
24{
25    return ^{
26        // iterate through all documents and photos and index them in spotlight
27    };
28}
29 
30@end
1class AppUpdate_001_001_000: NSObject, AppUpdate {
2    func version() -> String {
3        return "001-001-000"
4    }
5 
6    func canExecuteUpdate() -> Bool {
7        if #available(iOS 9.0, *) {
8            return true
9        } else {
10            return false
11        }
12    }
13 
14    func stepName() -> String {
15        return "upd-\(version())"
16    }
17 
18    func stepDescription() -> String {
19        return "Index documents and photos in spotlight."
20    }
21 
22    func updateBlock() -> (() -> Void) {
23        return {
24            // EXAMPLE: iterate through all documents and photos and index them in spotlight
25        }
26    }
27}

version method returns a string in format ###-###-### which is used to sort updaters chronologically. canExecuteUpdate is a place where the update step can decide whether to be executed or not (ie it only makes sense to execute on certain iOS versions because it relies on API introduced in relevant iOS version).
stepName returns a String which needs to be unique across all update step instances. This is because stepName String is used as a key in UserDefaults where we track which steps have been executed so far.
stepDescription is a short description of what the update step does (usually one sentence). It is stored as a value in UserDefaults.
updateBlock returns the block which does the actual job of whatever the update step should do. Some examples include:

  • resetting cached images to force the app to retrieve new shiny images from the server
  • indexing content in Spotlight
  • database scheme update / data migration in case you don’t use CoreData (which you probably should), but use something like FMDB
  • complete removal of local DB and other data forcing app to retrieve everything from the server again

Benefits

  • By wrapping each update step into its own class keeps the code clean and makes unit-testing easier.
  • Adding a new update step boils down to creating one class which implements the AppUpdate protocol.

Alternatives

Alternatively to the class-based approach described in this article, a block-based approach can be used. Such an approach is used by MTMigration utility.

Useful links

https://en.wikipedia.org/wiki/Single_responsibility_principle
https://en.wikipedia.org/wiki/Reflection_(computer_programming)
https://github.com/mysterioustrousers/MTMigration

share post

Likes

0

//

More articles in this subject area

Discover exciting further topics and let the codecentric world inspire you.

//

Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.