iOS Text-Updates via Server

Manchmal hat man eine App im Store und möchte live noch Änderungen an Texten vornehmen. Die App kann natürlich Texte von einem Server nachladen, aber das Programm-Bundle selbst kann nicht verändert werden. Neben der App liegen allerdings noch drei weitere Verzeichnisse, die die App benutzen darf, hier am Beispiel des Simulators:

GoldCoast:7AFD1B47-C759-488F-883F-8718B33FCB4D macmark$ pwd
/Users/macmark/Library/Application Support/iPhone Simulator/6.1/Applications/7AFD1B47-C759-488F-883F-8718B33FCB4D
GoldCoast:7AFD1B47-C759-488F-883F-8718B33FCB4D macmark$ ls -1F
Documents/
Library/
TextUpdates.app/
tmp/
			

Wenn der Berg nicht zum Propheten kommt …

In Documents kann sie auch schreiben. Daher muß der Download von Text-Updates in das Documents-Verzeichnis erfolgen. Man könnte nun für jedes Text-Element in der GUI einzeln nachsehen, ob ein neuer Text heruntergeladen wurde, und das GUI-Element dann mit dem Text aktualisieren. Aber das ist uncool, weil es die verfügbare Automatik zum Textladen nicht verwendet und damit zu viel Arbeit kostet und zudem viel Code an vielen Stellen erfordern würde. Wie geht es besser?

Normalerweise lädt man seine Texte eh aus der Localizable.strings Datei mit der Funktion NSLocalizedString oder mit NSLocalizedStringFromTable, wenn man eine andere Datei im App-Bundle für die Strings verwendet. Lokalisierte Ressourcen liegen in der App in Sub-Verzeichnissen wie de.lproj und en.lproj. Die Sprach-Definition kann sogar bis zu drei Ebenen haben. Hier nur ein Beispiel für zwei Ebenen: en_us.lproj.

Und falls man "Use Base Internationalization" im Projekt aktiviert hat und für seine XIBs ebenfalls "Localizable Strings" in Localization eingestellt hat, dann hat man in den Sprach-Verzeichnissen (*.lproj) auch Strings-Dateien für die jeweiligen ViewController:

GoldCoast:7AFD1B47-C759-488F-883F-8718B33FCB4D macmark$ ls -1F TextUpdates.app/*.lproj
TextUpdates.app/Base.lproj:
ViewController.nib

TextUpdates.app/de.lproj:
Localizable.strings
ViewController.strings

TextUpdates.app/en.lproj:
InfoPlist.strings
Localizable.strings
ViewController.strings
			

Aber im App-Bundle, von wo diese Dateien normalerweise geladen werden, kann man sie nicht aktualisieren. Also sind noch drei Aufgaben zu lösen:

Sprachdateien kopieren in schreibbares Verzeichnis

Um die Sprachdateien aktualisieren zu können, kopiere ich sie in das Documents-Verzeichnis:

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [[paths objectAtIndex:0] stringByAppendingString:@"/"];


NSString *bundleRoot = [[[NSBundle mainBundle] bundlePath] stringByAppendingString:@"/"];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSArray *dirContents = [fileManager contentsOfDirectoryAtPath:bundleRoot error:nil];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"self ENDSWITH '.lproj'"];
NSArray *allLprojDirs = [dirContents filteredArrayUsingPredicate:predicate];

NSLog(@"alldirs %@", allLprojDirs);
[allLprojDirs enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    NSError *error;
    NSString *fromPath = [bundleRoot stringByAppendingString:[obj description]];
    NSString *toPath = [documentsDirectory stringByAppendingString:[obj description]];

    // All *.lproj dirs with their strings files are only copied one time:
    // If the dirs in Documents do not exist.
    if (![fileManager fileExistsAtPath:toPath]) {
        NSLog(@"Copying from %@ to %@", fromPath, toPath);
        [fileManager copyItemAtPath:fromPath
                             toPath:toPath
                              error: &error];
        if (error) {
            NSLog(@"%@", [error localizedDescription]);
        }
    }
    
}];
// Replace those files with updates from some server later as you like by downloading updated versions.		    
		    

Anschließend sieht das Documents-Verzeichnis so aus in diesem Beispiel:

GoldCoast:7AFD1B47-C759-488F-883F-8718B33FCB4D macmark$ ls -1FR Documents
Base.lproj/
de.lproj/
en.lproj/

Documents/Base.lproj:
ViewController.nib

Documents/de.lproj:
Localizable.strings
ViewController.strings

Documents/en.lproj:
Localizable.strings
ViewController.strings

Lokalisierte Strings aus Documents lesen

Das standardmäßige Makro NSLocalizedString liest ja aus dem App-Bundle. Ich benutze eine andere Version des Makros und lese damit aus dem Documents-Verzeichnis:

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
self.documentsBundle = [NSBundle bundleWithPath:documentsDirectory];

NSString *message = NSLocalizedStringWithDefaultValue(@"animatedText",
                                                   nil,
                                                   self.documentsBundle,
                                                   @"localized string not found",
                                                   nil);
	

Damit kann man alle Fälle abdecken, die explizit Strings laden. Was bleibt, ist das automatische Laden von Strings für Nibs.

Automatisches Laden von Strings durch ViewController aus Documents

Wird das Nib für einen ViewController geladen, das lokalisierte Strings benutzt, dann werden diese im gleichen Bundle gesucht. In meinem Beispiel liegt das Nib (die lebend schockgefrosteten serialisierten GUI-Objekte) im Base.lproj-Verzeichnis neben den lokalisierten Strings im gleichen Bundle. Normalerweise würden das Nib und die Strings aus dem App-Bundle geholt, wenn man den zugehörigen ViewController auf die übliche Weise instanziiert.

Ich verwende jedoch auch hier eine Variante, bei der ein anderes Bundle zur Instanziierung des ViewControllers angegeben wird:

//    self.viewController = [[ViewController alloc] initWithNibName:nil bundle:nil];
NSBundle *bundle = [NSBundle bundleWithPath:documentsDirectory];
self.viewController = [[ViewController alloc] initWithNibName:nil bundle:bundle];
	

Die auskommentierte Version würde das passende Nib und Strings aus dem App-Bundle laden. Die andere Version lädt alles aus dem Documents-Verzeichnis.

Zusätzliche Hinweise

Hier ist mein Beispiel-Xcode-Projekt Version 2. Viel Spaß!

Valid XHTML 1.0!

Besucherzähler


Latest Update: 11. September 2015 at 19:50h (german time)
Link: warcraft.realmacmark.de/dev/osx_dev_text_via_server.php