The Serious Business of the Idiot Detector (Part 3)

About 80 days ago Dr. Ivan Egghead's Amazing Idiot Detector went on sale on the App Store for $0.99. In part 1 of this series I explained how this was an experiment to experience selling on the App Store and various marketing ideas. Part 2 of this series showed what happened when we took out an online advert. Now in part 3 we're going to explore what happened when we released a free 'Lite' version to promote the full paid-for version.

The Lite version has just one theme - the one that looks and sounds like a military sonar device. To keep things simple, we only included the accelerometer control mode. The other thing we did was to stick a big full screen advert at start-up to directly promote the full paid-for version. So what happened?

Well, we were very happy with our download stats for the free version. In fact we were pleasantly surprised! We reached No. 1 in the Entertainment section and around No. 4 overall in several European countries. In contrast we also received incredibly bad ratings and pretty vicious comments on iTunes! It seems from the comments that users just didn't 'get' the subtle accelerometer control we had implemented.

There is plenty of evidence from various sources that releasing a free version is a good way to drive sales of a full paid-for product. And it makes sense - what better way to target your marketing than directly to those iPhone users who have shown enough interest in your product to have taken the time to download it! So, what was our experience?

No increase in sales at all. There are probably a couple of reasons for this. Firstly, we pulled the online advert around the same time as we published the Lite version; sales coming from this advert may simply have been replaced by sales from the Lite version. Secondly, if most people hadn't figured out the accelerometer control and thought the application just didn't work, they would not be happy to pay for the full version. Finally there is the fact that it's an Idiot Detector! It's just a bit of fun you might run occasionally for a laugh and if you can get it for free, why would you pay for it?

So what can we conclude?
  • Write software people actually place value in (i.e. not a Idiot Detector!).
  • Make it really easy to use and listen to your users' comments. If they're struggling to understand - it's your fault, not theirs!
  • People like free stuff - but if you're using a free version for promotion, make sure there is still value left in what you're selling.
  • People will be very vocal in their disapproval if you get it wrong and, because their reviews are pretty much anonymous, they can be quite abrasive. Although it may be hard to take, this is the most honest opinion you will receive. If you get it wrong you WILL be told, so get it right first time. A wider beta test would help.
  • The App Store is a huge place. Even with a silly little app like the Idiot Detector we are still attracting several thousand new users a week.
So, the experiment is over. What's next? We've withdrawn the paid-for version from the App Store but we're going to keep the Lite version available for a while. We've listened to the user feedback and are implementing a touch mode to make it easier to control. As a bonus we're also bundling a second theme into the Lite version. If you want to check it out, it is available here on the App Store.

Storing File References using Core Data

I was working on an application a while back which required file references to be stored by Core Data. These references needed to be fairly robust. If the user decided to move a file for example, the application needed to be able to locate that file in its new location. The answer was to use aliases, but as it took a bit of digging to work out how to do this I thought it would be worth sharing.

I think the easiest way to explain this is to walk through an example. However, if you want to skip the tutorial you can download the example project here. Please feel free to reuse any of this code (although I provide it with no warranty). If you do use it, or if you find this post useful, I would be love to hear from you - it’s nice to know you might have helped someone.

For the example I’m going to create a simple Core Data application with a table view displaying the full file paths of various files. To do this I need to represent the path in two ways; an alias to the file and a string containing the path. Only the alias will be stored by Core Data.

To start, create a new project in Xcode using the Core Data Application template. I’ve called my project FileRefs - inspired naming I’m sure you’ll agree.

In the data model file (FileRefs_DataModel.xcdatamodel) create a new entity. Set the name of this entity to FileData and the class to OPSFileData (we’ll create this class in a moment). Next, add a new attribute named aliasHandle to the entity. This attribute is going to store the file alias in the form of an NSData* object, so we set the type to Binary data. If a file is removed, or an invalid file path is entered, then this NSData* object will be nil, so we need to set the optional flag.

Add a new class named OPSFileData to the project derived from NSManagedObject and add the following properties:

@property (retain) NSData* aliasHandle;
@property (copy) NSString* filePath;
You will also need to include the definition for NSManagedObject:
#import <CoreData/CoreData.h>
The aliasHandle property is already in the data model so we declare it using a dynamic property:
@dynamic aliasHandle;
The filePath property will return the string representation of the file path for display in the table view. The OPSFileData class will implement the accessors for this property. The ‘get’ method will convert the aliasHandle into a string while the ‘set’ method will convert a string into an aliasHandle. Add these methods to the implementation as follows:
- (NSString*) filePath
{
// Get the alias from the data store
CFDataRef dataRef = (CFDataRef)[self aliasHandle];
if( dataRef == nil )
{
return @"Invalid file handle";
}


// Work out the size needed to hold the alias handle
CFIndex dataSize = CFDataGetLength( dataRef );
// Create the alias handle of the correct size
AliasHandle aliasHdl = (AliasHandle)NewHandle( dataSize );
if( aliasHdl == nil )
{
return @"Error creating file handle";
}


// Copy the contents of the data into the alias handle
CFDataGetBytes( dataRef, CFRangeMake( 0, dataSize ),
(UInt8*)*aliasHdl );

// Resolve the alias into a file reference
// We are using the FSResolveAliasWithMountFlags so that
// the user is not presented with a UI asking to mount
// drives etc. We could have used FSResolveAlias instead.
FSRef fileReference;
Boolean wasChanged;
OSErr err = FSResolveAliasWithMountFlags( NULL,
aliasHdl, &fileReference,
&wasChanged, kResolveAliasFileNoUI );
DisposeHandle( (Handle)aliasHdl ); // Finished with the alias handle

if( noErr != err )
{
return @"Unable to resolve alias";
}


// Convert the file reference into a string
UInt8 fileNameBuffer[1024];
OSStatus status = FSRefMakePath( &fileReference,
fileNameBuffer, 1024 );

if( status == noErr )
{
return [NSString stringWithUTF8String:(char*)fileNameBuffer];
}

else
{
return @"Unable to determine file path";
}

}


- (void) setFilePath:(NSString*)filePathAsString
{
// First convert the string into a file reference
FSRef fileReference;
Boolean isDirectory;
OSStatus status;
status = FSPathMakeRef( (UInt8*)[filePathAsString UTF8String],
&fileReference,
&isDirectory );
if( status != noErr )
{
[self setAliasHandle:nil]; // The string for the path is invalid
return;
}


// Create a new alias handle from the file reference
AliasHandle aliasHdl;
FSNewAlias( NULL, &fileReference, &aliasHdl );
if( !aliasHdl )
{
[self setAliasHandle:nil];
return;
}


// Convert the alias handle into a data reference
CFDataRef dataRef;
dataRef = CFDataCreate( kCFAllocatorDefault,
(UInt8*)*aliasHdl,
GetHandleSize( (Handle)aliasHdl ) );
DisposeHandle( (Handle)aliasHdl );

if( !dataRef )
{
[self setAliasHandle:nil];
}

else
{
// Set the aliasHandle property
[self setAliasHandle:(NSData*)dataRef];
CFRelease( dataRef );
}

}
The final thing to do is to hook up the user interface. Open the XIB file and add a new NSArrayController named FileData Array. In the inspector window for the array controller’s bindings, bind the Managed Object Context to the app delegate (FileRefs_AppDelegate). This will tell the array controller where to look for the managed object context. Switch to the attributes tab for the array controller and set the object controller’s mode to Entity. The entity name is FileData - this is the name of the entity we created earlier in the data model. Finally, set the Prepares Content check box to tell the array to load the data at start-up.

Add a NSTableView to the application window, turning off the headers and setting the number of columns to 1. In the binding for the table column (make sure you select the table column and not the view) bind the value to FileData Array. The controller key should be set to arrangedObjects and the model key path to filePath. This tells the table column that its contents will be provided by FileData Array’s arrangedObjects property and that each of the objects in this array has a filePath property.

Add two buttons to the window: Add and Remove. Control click on the Add button and drag to the FileData Array object to connect the button to the controller’s ‘add’ method. Similarly connect the Remove button to the controller’s ‘remove’ method.

You should now be able to build and run the application. Add a new item and type in the path to a file on your machine. Move this file and you should see this path updating as you switch focus back to your application.

That’s it. The user interface needs a little more work; the button states should be bound to the ‘canAdd’ and ‘canRemove’ methods on the array controller. And of course you’ll have taken Martin Pilkington’s pledge and will be making your app accessible.