4
\$\begingroup\$

On advice from an answer to my previous question, I have written some code that will allow my game balance properties to be loaded from a remote server. Right now I am just hosting the raw property list file on a web server for simplicities sake.

My first question is whether there are any long term problems associated with hosting the file that way. Users will access the remote file whenever a GameBalance object is created, but it is only downloaded if the version is higher than the one saved to the local documents folder.

My second question is whether I provided enough protection in the event of a failed connection, or in the event that only part of the property list is downloaded due to some internet related error. Do packet loss and internet protocols come into play here, and can they mess this up?

My final question is that while I was able to do almost all of the work happening inside the initialization in plain C functions, I do have one call to [self loadSettingsFromList] in there. Is this acceptable or is it bad practice? The only thing this method is doing is assigning values to instance variables.

Here is the code to download the property list:

#pragma mark - Download Plist  
int savedVersionNumber() {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString  *filePath = [NSString stringWithFormat:@"%@/%@", documentsDirectory,@"ServerGameBalance.plist"];
    NSDictionary *settingsList = [[NSDictionary alloc] initWithContentsOfFile:filePath];
    return [settingsList[@"VersionId"]intValue];
}
int remoteVersionNumber() {
    NSString *stringURL = @"http://192.168.1.1:80/DownloadedBalance.plist";
    NSURL  *url = [NSURL URLWithString:stringURL];
    NSDictionary *settingsList = [NSDictionary dictionaryWithContentsOfURL:url];
    return [settingsList[@"VersionId"]intValue];
}
void downloadPlistFromServerAndSave() {
    NSString *stringURL = @"http://192.168.1.1:80/DownloadedBalance.plist";
    NSURL  *url = [NSURL URLWithString:stringURL];
    NSData *urlData = [NSData dataWithContentsOfURL:url];
    if (urlData) {
        NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
        NSString *documentsDirectory = [paths objectAtIndex:0];
        NSString  *filePath = [NSString stringWithFormat:@"%@/%@", documentsDirectory,@"ServerGameBalance.plist"];
        [urlData writeToFile:filePath atomically:YES];
    }
}
NSDictionary* downloadedPlist() {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString  *filePath = [NSString stringWithFormat:@"%@/%@", documentsDirectory,@"ServerGameBalance.plist"];
    NSDictionary *settingsList = [[NSDictionary alloc] initWithContentsOfFile:filePath];
    return settingsList[@"Settings"];
}

And here is the initialization:

#pragma mark - Initialization
-(id) init {
    self = [super init];
    if (self) {
        //if it is zero then the internet could be down, or it is not valid anyway
        if (remoteVersionNumber() != 0) {
            //download a new plist if needed
            if (remoteVersionNumber() > savedVersionNumber()) {
                downloadPlistFromServerAndSave();
            }
            _listOfSettings = downloadedPlist();
        } else {
            _listOfSettings = localSettingsList();
        }
        [self loadSettingsFromList];
    }
    return self;
}
NSDictionary* localSettingsList() {
    NSString *plistCatPath = [[NSBundle mainBundle] pathForResource:@"DTGameBalance" ofType:@"plist"];
    NSDictionary *settingsList = [[NSDictionary alloc] initWithContentsOfFile:plistCatPath];
    return settingsList[@"Settings"];
}
-(void) loadSettingsFromList {
    _gameSettings = [_listOfSettings objectForKey:@"gameSettings"];
    _towerSettings = [_listOfSettings objectForKey:@"towerSettings"];
    _jobCostSettings = [_listOfSettings objectForKey:@"jobCostSettings"];
    _roomCostSettings = [_listOfSettings objectForKey:@"roomCostSettings"];
}
\$\endgroup\$
3
\$\begingroup\$

There's two important big picture things to grasp here.

First, rather than downloading the entire plist just to check the version number, we'd be well served by having a separate location to check the version number. All we need to do is download a very small package and compare the version. Then we only need to waste bandwidth for both the user and our server if there version is actually out of date.

Second, accessing remote files takes time. Accessing remote files also can have errors. We could completely fail to fetch the file. And, given that these files are purely game balance tweaks, I'd argue that it's not necessary to make the user wait while the file downloads. We need to move this operation to the background.

The class that is in charge of checking for and fetching updates should also be completely different from the class that loads are already downloaded settings from a file.

Basically, we have a given file path where we store our game balance settings. On the apps first launch, we copy the plist into this file path. Now when the user starts a new game, we load the settings from this file path every single time. Meanwhile, we're asynchronously looking for updates. If we find that there are updates, we download the updated plist and write it to the predetermined file path where we know our other class fetches the settings from.

So, step one is to write some logic that moves the pre-included plist to a file path where you can overwrite it.

Step two, move all the logic of checking online for updates out of the class that loads the settings from this file path.

Step three, create a new class for checking for online updates.

This is a big step. We need to decide how frequently the app should check, and make sure we schedule the class to check with that frequency. If I play your app for 6 hours today, does my device need to check every 10 minutes? Unlikely. It's unlikely that you'll update more than once a day, right? And it's also unlikely that your game balance tweaks will be anything that needs an immediate tweak, right? Heck, you could even include in the game balance settings a value to determine when the next check for updates should be.

But when we create this class, we actually shouldn't do a whole lot in the init method. The init method should really only do one thing: initialize an instance variable with the current version of the game.

The class should also have a readonly NSDate property to store the timestamp of its last successful check of the online file.

Now, we just provide a method, something like: fetchUpdates which first checks URL path A to compare the current version to the most up to date online version, and if it looks like the online version is more recent, then the method then initiates a connection to URL path B where it actually downloads the update.

Additionally, we will want to either write the class so it has a delegate to inform the update is complete or our fetchUpdates method should take a completion block argument that will fire once the updates our complete.

And rather than using NSData's dataWithContentsOfURL: method, we need to use a NSURLConnection object and make our class an NSURLConnection delegate so we can perform all the network operations asynchronously.

\$\endgroup\$
  • 1
    \$\begingroup\$ I should have realized that by cramming the downloading code into this class I was violating the single responsibility principle. Your explanation of how to set things up the right way is very succinct and much appreciated. \$\endgroup\$ – bazola Aug 16 '14 at 13:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.