A Downloader Controller
Back in the first article in this series we created a BRControl wrapper around the ProgressBar widget. I said then that we would put this to use shortly, and so we will. In today's tutorial we will look at a more substantive example of plugin development, including localizations and resources, some preferences, and asynchronous download from the internet.
Our end result will be a controller which downloads from a URL stored in our preferences, displaying the progress of the download as it does so. It will also show a method for supporting resumption of an interrupted download.
Plugin Developers' Toolkit
Since we're going to look at a real-world example today, it seems right that we introduce some important items for your toolkit: resources, localizations, and the preferences system.
This is the simplest of the three; the interface to access your own resources and those of the BackRow framework is simply the NSBundle class. For you own resources, you fetch the bundle containing your own class, like so:
[[NSBundle bundleForClass: [self class]] pathForResource: name of type: type];
For an object within the BackRow framework itself, such as a standard image, the intro movie or the strings file containing error descriptions, use the backRowFramework( ) function, like so:
[backRowFramework( ) pathForResource: name ofType: type];
Having retrieved a path to the item in question, you can then access the resource directly, or create a URL using [NSURL fileURLWithPath:] as appropriate.
Localization is handled mostly by the BRLocalizedStringManager class. It offers four different functions for locating localized strings:
+ (NSString *) backRowLocalizedStringForKey: (NSString *) key inFile: (NSString *) stringsFile; + (NSString *) applicationLocalizedStringForKey: (NSString *) key inFile: (NSString *) stringsFile; + (NSString *) appliance: (id) anyObjInAppliance localizedStringForKey: (NSString *) key inFile: (NSString *) stringsFile; + (NSString *) localizedStringForKey: (NSSrting *) key inFile: (NSString *) stringsFile fromBundle: (NSBundle *) bundle;
The base routine used to fetch the string is +localizedStringForKey:inFile:fromBundle:, and the other three simply pull together the parameters for that call. The +applicationLocalizedStringForKey:inFile: call is used to locate a string within the bundle of the actual current application (i.e. the Finder). The other two are used to fetch data from the BackRow framework or from the bundle of the calling appliance. For strings within your appliance's default strings file 'Localizable.strings', you would use:
NSString * localized = [BRLocalizedStringManager appliance: self localizedStringForKey: @"SomeString" inFile: nil]
If you have other strings files, you can specify the name of the one you want (minus the '.strings' extension) in the last parameter of that call.
So, we have an easy way of reading our own localized strings, but that's only good for reading. When we use the NSLocalizedString() macro normally, we are able to use the genstrings command-line utility to generate the strings files themselves. Fortunately, genstrings can be told to look for a different form of macro from the default 'NSLocalizedString', meaning we can define our own BackRow-based versions. To do this, create a file in your project called something like 'BRLocalizations.h', and put this inside it:
#import <BackRow/BRLocalizedStringManager.h> #define BRLocalizedString(key, comment) \ [BRLocalizedStringManager appliance:self localizedStringForKey:(key) inFile:nil] #define BRLocalizedStringFromTable(key, table, comment) \ [BRLocalizedStringManager appliance:self localizedStringForKey:(key) inFile:(table)]
You can then run the following command from your poject directory to generate your strings files:
genstrings -s BRLocalizedString -o English.lproj/
You can, as always, use NSUserDefaults to manage your own preferences. However, BackRow provides an interface for both the FrontRow preferences and those for specific named domains. To retrieve a value from the FrontRow preferences you can use the RUIPreferences object:
int displayID = [[RUIPreferences sharedFrontRowPreferences] integerForKey: @"FrontRowUsePreferredDisplayID"];
To use a specific preference domain, you can use the 'RUIPreferenceManager' object:
int displayID = [[RUIPreferenceManager sharedPreferences] integerForKey: @"FrontRowUsePreferredDisplayID" domain: @"com.apple.frontrow"];
Both classes provide synchronization facilities. RUIPreferences provides -setSynchronizeOnWrite: (BOOL) sync and -syncNow while RUIPreferenceManager provides the -syncDomain: (NSString *) domain function.
This section isn't much to do with BackRow, admittedly, but it is useful in the context of the tutorial in general, since we'll be using this method to locate the folder used to hold our downloads.
The file at <Foundation/NSPathUtilities.h> contains the values you'll need for this, and a handy Objective-C wrapper for the NSSystemDirectories API (in <NSSystemDirectories.h>). You can use this to get lists of directories matching certain criteria. Within that header file you'll find two enumerations; the top one identifies a specific folder (Application Support, Documents, Library, etc.), and the lower one specifies masks for the different domains in which they can exist (System, Local, User, Network, etc.). So, to get a list of all paths for the Application Support folders in the Local and User domains, you would use:
NSArray * searchPath = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask | NSLocalDomainMask);
This call would return the following list of paths:
/Users/[username]/Library/Application Support /Library/Application Support
In the main example, we'll use this to place our downloaded data into the user's Caches folder.
To download data to a file (as opposed to simply retrieving an NSData object) we'll use the NSURLDownload class. This supports resumption of data and also in-transit decoding of certain MIME types: MacBinary, BinHex, and GZip. Note that partially-downloaded files decoded from Gzip format cannot be resumed, so if resumption is more important you might choose to disable decoding of that MIME type by implementing -download:shouldDecodeSourceDataOfMIMEType: and returning NO for the Gzip MIME type.
To begin a new download, you need to create an NSURLRequest and hand that to the NSURLDownload constructor. Since we would like to resume any partial downloads, we'll tell the downloader not to delete the file when it fails (or is cancelled).
NSURLRequest * request = [NSURLRequest requestWithURL: url cachePolicy: NSURLRequestUseProtocolCachePolicy timeoutInterval: 20.0]; NSURLDownload * download = [[NSURLDownload alloc] initWithRequest: request delegate: self]; [download setDeletesFileUponFailure: NO];
To resume an existing download, we can pass the resume data (from a prior call to NSURLDownload's -resumeData method) into a new NSURLDownload instance. If the initializer returns nil in this case, resumption wasn't possible and the download must be restarted from the beginning.
[[NSURLDownload alloc] initWIthResumeData: data delegate: self path: downloadedFilePath];
The Download Controller
Unlike the last couple of examples, there is a fair amount of code involved in this class, so I won't include all of it here. However, you can download the sample project provided at the end to see the whole thing. Here I will include the main parts needed for starting the download and maintaining progress, but I'll leave out some of the NSURLDownload delegate methods and such.
Our class will need a few member variables to maintain state nicely. Firstly we'll have some UI controls:
BRHeaderControl * _header; BRTextControl * _sourceText; QuProgressBarControl * _progressBar;
We will also need some items to manage the download itself:
NSURLDownload * _download; NSString * _outputPath;