Programmatically count App Store ratings

Overcast, one of my favorite iOS apps, has a thing where it shows how many App Store ratings it has. I don’t know if it was the first app to do this, but it was the first I’d seen. Anyway, I wanted that functionality in one of my apps.

As it turns out, the App Store has an API for getting this information. All you need to do is download from https://itunes.apple.com/lookup?id=APP_ID (where APP_ID is the ID of the app you’re interested in). This gives you a pile of JSON which contains, among many other things, the count of ratings for the app.

So, download some JSON over https, and parse it. Simple. I wrote a little class to do this. There’s one function, which looks something like this:

func fetchNumberOfRatings(appID: String, completion: @escaping (Bool, Int) -> ()) {
	
    let appStoreURL = URL(string: "https://itunes.apple.com/lookup?id=\(appID)")
    let task = URLSession.shared.dataTask(with: appStoreURL!, completionHandler: { (data, response, error) in
		
        var gotResult = false
        var ratingsCount = 0
		
        if error == nil && data != nil,
            let jsonResult = (try? JSONSerialization.jsonObject(with: data!, options:[])) as? NSDictionary,
            let results = jsonResult["results"] as? NSArray,
            results.count > 0,
            let result = results[0] as? NSDictionary,
            let numberOfTimes = result["userRatingCountForCurrentVersion"] as? Int
        {
            // Got it!
            gotResult = true
            ratingsCount = numberOfTimes
        }
		
        DispatchQueue.main.async {
            completion(gotResult, ratingsCount)
        }
    })
	
    task.resume()
}

It’s pretty self-explanatory. It uses NSURLSession to download the JSON, parses it with NSJSONSerialization, and calls a completion proc you provide. You’d call it like:


FARatingCounter.defaultCounter.fetchNumberOfRatings(appID: appInfo.ID) {
    success, number in
    // Do something with number
}

I just posted this to GitHub, along with a little sample app showing how to use it. Check it out here.

Random metrics for an app update

I just finished up a very large update to PuzzleTiles, and wanted to share a few development metrics.

The previous shipping version comprised 6,181 lines of Objective-C code in 27 files (228 lines per file), plus 559 lines in 26 header files (21 lines per file). This is actual LOC (as measured by cloc), not including comments or blank lines.

The new update comprises 7,394 lines of code: 6,417 lines of Objective-C code in 24 files (267 lines per file), plus 388 lines in 22 header files (17 lines per file), and finally, 589 lines of Swift code in 15 files (39 lines per file)

Objective-C C Headers Swift Total
PuzzleTiles 1.2.4 27 files, 6,181 lines 26 files, 559 lines N/A 53 files, 6,740 lines
PuzzleTiles 2.0 24 files, 6,417 lines 22 files, 388 lines 15 files, 589 lines 61 files, 7,394 lines

The largest file was (and is) PuzzleViewController.m, the file that holds the main view controller in the app, at 962 lines of code. Not too bad, I think.

From start to finish, the update took me around 258 hours. This includes everything related to the app: research, design, development, testing, artwork, screenshots and videos for iTunes Connect, etc.. Had I done this full time (which I did not), it would have taken just over 6 weeks. That's actually kind of a long time; more time than I usually spend on an app update. To be fair, though, this was a huge update.

When I took my car to the dealer for its 100,000 mile service, the dealer told me it would cost nearly $1,000. I asked what they could possibly be doing to make it cost that much. He said they intended to basically lift up the radiator cap, and pull a new car underneath it. So it was with this app update.

I overhauled the entire UI – previously it was still iOS6-vintage. I converted everything to use Storyboards and Autolayout, adding support for all the bigger phones. I added 3x graphics for iPhone 6 Plus. I added a new tile set, and updated all the others to look better (and have 3x). I added in-app purchase, and iCloud syncing (both things with which I previously had no experience). I removed every deprecated call, and added some new APIs introduced in iOS7 and iOS8 where it made sense to do so. I rewrote a few problem areas entirely (such as my GameKit code), and made some nice enhancements to other areas (the control scheme, and sound playing, among others).

PuzzleTiles was my first iOS app, originally released in 2010, and last updated in 2012. The guy that wrote it had much less expertise than I currently possess. It was very fun to get in there and redo things with the benefit of the knowledge I've gained in the years since.

I'm reasonably sure this level of effort won't turn out to have made sense, financially. In that regard, I'd likely have done better to spend that time on other apps that actually do make money. But money wasn't the point here. I wanted PuzzleTiles to once again be an app I could be proud of. In that regard, it's already a success.

Of course, it won't hurt if you download it, and buy the in-app-purchase. :)

Regarding Swift

I haven't yet written anything here about Swift. It's not because I haven't played with it (I have), and it's not because I don't have thoughts on it (I do). I just feel like, until I've actually shipped an app using it, I'm not qualified to write about it with any level of authority. And at the moment, I've not shipped an app using Swift.

That's about to change. I'm working on a long-overdue update to PuzzleTiles. I decided from the start not to rewrite any existing code in Swift. What I have been doing, however, is writing all new code in Swift.

This has taught me a lot about Swift; especially, how Swift and Objective-C interoperate. I'll be posting more about this later. And, from this point forward, any code in my posts will be almost certainly be Swift, not Objective-C.

High resolution timing in Cocoa, revisited

A while back I wrote about high resolution timing in Cocoa. It's been a while, and I've updated it a bit – adding ARC support, adding a call to time a block, and removing some stuff that wasn't needed. Hopefullly you find it useful.

// MachTimer.h
#include <mach/mach_time.h>

@interface MachTimer : NSObject

+ (instancetype) timer;
+ (NSTimeInterval) timeForBlock:(void (^)(void))block;

- (void) start;
- (NSTimeInterval) elapsedSeconds;

@end


// MachTimer.m
#import "MachTimer.h"

static mach_timebase_info_data_t timeBase;

@implementation MachTimer
{
    uint64_t timeZero;
}   

+ (void) initialize
{
    (void) mach_timebase_info( &timeBase );
}

- (instancetype) init
{
    self = [super init];
    if( self ) {
        [self start];
    }
    return self;
}

+ (instancetype) timer
{
#if( __has_feature( objc_arc ) )
    return [[[self class] alloc] init];
#else
    return [[[[self class] alloc] init] autorelease];
#endif
}

+ (NSTimeInterval) timeForBlock:(void (^)(void))block
{
    MachTimer* aTimer = [self timer];

    [aTimer start];
    block();
    return [aTimer elapsedSeconds];
}

- (void) start
{
    timeZero = mach_absolute_time();
}

- (NSTimeInterval) elapsedSeconds
{
    return ((NSTimeInterval)(mach_absolute_time() - timeZero)) * ((NSTimeInterval)timeBase.numer) / ((NSTimeInterval)timeBase.denom) / 1000000000.0f;
}

@end

You might use it like this:

MachTimer* aTimer = [MachTimer timer];
[self doSomeLengthyOperation];
NSLog( @"Lengthy operation took %f seconds", [aTimer elapsedSeconds] );

Or, with the new block call:

NSTimeInterval theTime = [MachTimer timeForBlock:^{
    // Do some lengthy operation.
}];
NSLog( @"Lengthy operation took %f seconds", theTime );

Credit where credit is due

I originally found the basis for this code on the Apple message boards, here. I dunno who wrote that, but thanks, dude. If you're that guy, let me know, and I'll be happy to give you attribution. Since writing this, I've discovered Waffle Software has a similar thing, which has some additional functionality; check that out if you're interested.

If you want to follow me, I'm @zpasternack on Twitter and on app.net.

Relative date formatting like Mail.app

Mail.app on Mac OS has a cool way of showing you relative dates of messages. Messages received today show only the time (e.g., "12:55 PM"), whereas messages received prior to today show only the date (e.g., 3/13/14), and if the date was Yesterday, it shows that, rather than the date. I wanted this same functionality in one of my apps.

Googling around for a bit turned up this StackOverflow question, with which I was not happy. So I did it myself (and, of course, added my own answer to that question). Here's what I did.

NSDateFormatter (as of Mac OS 10.6 and iOS4) does relative date formatting (e.g., "Today", "Yesterday" for you, like so:

NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
[formatter setTimeStyle:NSDateFormatterNoStyle];
[formatter setDateStyle:NSDateFormatterShortStyle];
[formatter setDoesRelativeDateFormatting:YES];

NSString* dateString = [formatter stringFromDate:[NSDate date]];    
NSLog( @"date = %@", dateString );

Which outputs:

2014-03-15 15:26:37.683 TestApp[1293:303] date = Today

But that still leaves the issue of instead using the time if the date is today. First, we need to be able to tell if a given date is today. For this I implemented a method isToday, as a category on NSDate. (Side note: I freaking love categories!)

@implementation NSDate (IsToday)

- (BOOL) isToday
{
    NSCalendar* calendar = [NSCalendar currentCalendar];

    // Components representing the day of our date.
    NSDateComponents* dateComp = [calendar components:NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit
                                             fromDate:self];
    NSDate* date = [calendar dateFromComponents:dateComp];

    // Components representing today.
    NSDateComponents* todayComp = [calendar components:NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit
                                              fromDate:[NSDate date]];
    NSDate* todayDate = [calendar dateFromComponents:todayComp];

    // If the dates are equal, then our date is today.
    return [date isEqualToDate:todayDate];
}

@end

Then, I made two date formatters, one for the day, and one for the time, and decided which to use based on whether the date is today or not. Like so:

- (NSString*) postDateToString:(NSDate*)aDate
{
    static NSDateFormatter* todayFormatter = nil;
    if( todayFormatter == nil ) {
        todayFormatter = [[NSDateFormatter alloc] init];
        [todayFormatter setTimeStyle:NSDateFormatterShortStyle];
        [todayFormatter setDateStyle:NSDateFormatterNoStyle];
    }

    static NSDateFormatter* notTodayFormatter = nil;
    if( notTodayFormatter == nil ) {
        notTodayFormatter = [[NSDateFormatter alloc] init];
        [notTodayFormatter setTimeStyle:NSDateFormatterNoStyle];
        [notTodayFormatter setDateStyle:NSDateFormatterShortStyle];
        [notTodayFormatter setDoesRelativeDateFormatting:YES];
    }

    NSDateFormatter* formatter = notTodayFormatter;

    if( [aDate isToday] ) {
        formatter = todayFormatter;
    }

    return [formatter stringFromDate:aDate];
}

And there you have it.

If you want to follow me, I'm @zpasternack on Twitter and on app.net.

Accessing the *real* home folder from a sandboxed app

The preferred way to find a path for a directory within the user’s folder is to use NSSearchPathForDirectoriesInDomains (or an NSFileManager equivalent, such as URLsForDirectory:inDomains:). Problem with that is, if your app is sandboxed, these fuctions won’t give you paths the actual directories you’ve asked for, but rather the equivalent paths within your app’s container, even if you’re using entitlements which allow access to those paths.

So, let’s say you want to get the path for the user’s Documents directory. You’d end up with something like this:

- (NSURL*) getDocumentsDirectoryURL
{
    NSArray* paths = NSSearchPathForDirectoriesInDomains( NSDocumentDirectory, NSUserDomainMask, YES );
    NSString* documentsPath = paths[0];
    return [NSURL fileURLWithPath:documentsPath];
}

You might expect to get back something like file://localhost/Volumes/SSD/Users/zach/Documents/ (at least, you’d expect that if your name was Zach). But you might be surprised when you actually get something more like file://localhost/Volumes/SSD/Users/zach/Library/Containers/com.mycompany.myapp/Data/Documents/.

If you really want the path to the real Documents directory (in ~/Users/zach/Documents), you need to do something like this:

- (NSURL*) getDocumentsDirectoryURL
{
    struct passwd *pw = getpwuid(getuid());
    NSString* realHomeDir = [NSString stringWithUTF8String:pw->pw_dir];
    NSString* documentsPath = [realHomeDir stringByAppendingPathComponent:@"Documents"];
    return [NSURL fileURLWithPath:documentsPath];
}

Admittedly, there aren’t many cases where you would want to do this. One reason you’d do this is to set the default location for an open/save file dialog – navigating the user to the Documents directory in your app container would be quite confusing. In my case, VideoBuffet really wants to find all the movies in your Movies folder, and there of course aren’t any in the Movies folder equivalent of the app container.

Notably, if you’re doing something like getting the path to Caches or Library (say, to save some settings into a .plist), you definitely want to use the normal NSSearchPathForDirectoriesInDomains method, because those should be saved into the app conainer.

Update 05/11/17:This is still a valid thing to do today, and here’s how you’d do it in Swift 3:


func getDocumentsDirectoryURL() -> URL {
	let pw = getpwuid(getuid());
	let home = pw?.pointee.pw_dir
	let homePath = FileManager.default.string(withFileSystemRepresentation: home!, length: Int(strlen(home)))
	let documentsPath = (homePath as NSString).appendingPathComponent("Documents")
	return URL(fileURLWithPath: documentsPath)
}

VideoBuffet 1.1 is released…

…in the Mac App Store. This release includes a number of new features, including setting of Finder Labels, preserve aspect ratio option, an improved random algorithm, and more. There are also big performance improvements for finding movies, and a bunch of bug fixes.

We’re very excited about this release, and recommend the update for all users.

Check out the app page, the full release notes, or buy it now.

Sparrow’s not a Canary

All the cool kids are talking about Sparrow’s acquisition by Google. People far smarter than I have chimed in about every aspect of the situation. But this caught my attention today: David Barnard, AppCubby:

…Sparrow is the proverbial canary in the coal mine. The age of selling software to users at a fixed, one-time price is coming to an end. It’s just not sustainable at the absurdly low prices users have come to expect. Sure, independent developers may scrap it out one app at a time, and some may even do quite well and be the exception to the rule, but I don’t think Sparrow would have sold-out if the team — and their investors — believed they could build a substantially profitable company on their own. The gold rush is well and truly over.

It’s true that the App Store has reset people’s expectations with regard to the value of software, and I do believe that the race to the bottom is ultimately detrimental to indie developers. It is pretty apparent, based on what we know, that the Sparrow team didn’t believe they could sustain a profitable business. But, how did we make the jump from “Sparrow could not sustain a profitable business building software” to “It’s not possible to sustain a profitable business building software”? I don’t see how Sparrow’s failure to build a sustainable business means that it’s impossible to do so. Remember how 60% of developers don’t break even? Sparrow had a 5 person development team, and was selling a $3 app. Read that again, because it’s kind of important. If you have a 5 person dev team, and you’re selling a $3 app, you probably plan on selling a whole lot of apps. Between Macs and iOS devices, there are over 200 million potential devices that can run Sparrow. And every single one of them ships with a very good email client. Are you seeing the problem yet? I believe Sparrow’s failure to build a sustainable business can be attributed to: 1. Overestimating the market for email client software. 2. Underestimating the value of their software to it’s actual target market.

I consider myself a nerd. I have a full-time job as a software developer, and I’ve got my own indie dev business going on the side. I have six (count ’em) active email accounts, and regularly send and receive many dozens of emails per day. But I don’t own Sparrow. Why? Apple’s Mail is good enough for me. Sparrow solves a problem I don’t have. I don’t think I’m alone. Mail is actually a pretty good app. I don’t think it even occurs to most people to go looking for a better email app, just as it doesn’t occur to most people to go looking for a better web browser.

The Twittersphere exploded in a shitstorm of angry sentiment from butt-hurt Sparrow users lamenting the ultimate demise of their software. Clearly, this was a very important piece of software for those that did buy it. I’m willing to bet that most of the people that paid $3 (or $10) for Sparrow would gladly have paid much more.

Brent Simmons knew this, years ago:

I’ve been joking for years that I’m going to write an email client and charge $500 for it — an email client that actually meets the needs of developers and professionals who rely on email, folks who type for a living.

I read somewhere that this may have been the genesis of the idea for Sparrow. I hope not, because if it was, they clearly didn’t read the next paragraph:

But I’m not going to, and I don’t know anybody who is. The economics of it make it kind of tough, given that Apple ships a good email client with OS X.

Sparrow was a premium product, sold at a commodity price. If BMW started selling their cars for $15,000, I don’t think it would surprise anyone when they lost buckets of money, and I don’t think anyone would declare that it’s not possible to sustain a profitable business selling cars.

Marco Arment nailed it:

Don’t blame Sparrow. Blame the terrible market for email clients.

Just because most apps are $0.99, doesn’t mean yours has to be. What is your software worth to your customers?

Seth Godin said,

“The problem with the race to the bottom is that you might win…”

If you want to follow me, I’m @zpasternack on Twitter and on app.net.

C Arrays: Here There Be Dragons

I was just answering a question on Stackoverflow (funny how often that inspires me to blog something) about using a C array in a Cocoa app.

The short answer is, you almost never want to do that. Sure, there are times when you might (if you’re going to end up passing it to some C function that needs a raw array; OpenGL or cocos2d, perhaps). In general, though, using some kind of container object is almost always a better solution.

This issue hit home for me, not too long ago. While developing PuzzleTiles, we needed a data model to store game state. Hmm, a two dimensional grid of tiles? My knee-jerk reaction was to type this:

const int MAX_ROWS = 8;
const int MAX_COLUMNS = 8;
@interface GameState : NSObject
{
    // stuff
    tileGrid[MAX_ROWS][MAX_COLUMNS];
    // more stuff
}
@end

Almost as soon as I typed this, I got this terrible sense of dread. As if a million memory buffers cried out, then were suddenly silenced, due to overflow.

But then I came to my senses. I got my big boy pants on, see? I’m an old hand, a seasoned vet, I know damn well what I’m doing. Right? Right.

I made sure to be very careful. tileGrid had no outside accessors; its state was returned via public member functions like \- (int) getTileAtX:(int)x y:(int)y Only GameState could muck with the grid, internally, and I was very careful about that. All accessors did sanity checks on all input and output parameters. I wrote code that, in Debug builds, would do an internal consistency check on tileGrid after each tile move operation, to make sure nothing went bad. I knew I had this stuff figured out.

Weeks passed. All memories of tileGrid faded away; we had other issues we were dealing with as we neared our release. GameState was rarely even looked at; it was a pretty simple piece of code, and was (as far as we knew) rock solid. And yet… we were plagued with very rare, seemingly random crashes. Sometimes we’d see them once or twice in a day, sometimes not for several days in a row. If we ever did get to see one in the debugger, or view a crash log, it was complete nonsense. Code that should never crash, was. And it was rarely the same piece of code that failed. It was maddening. For weeks we fought this, scrutinizing every memory allocation and release; running over and over again in Leaks; poring over all our memory management code for any hint of trouble.

In desperation, I eventually returned to GameState. I added some extra consistency checking, and… saw it fail within a minute of running the game. It turned out my original consistency checking code was missing one corner case, and it turned out that case happened pretty often. When it did fail, one part of GameState’s internal code would fail to find a tile, and return the invalid value {-1, -1}. Then another part of the code would not bother to check for the invalid value, and would happily use it to index into tileGrid, accessing tileGrid[-1][-1], effectively randomly overwriting 4 bytes of whatever poor object happened to be 36 bytes before it. Now the random maddening crashes were completely clear.

It wasn’t all bad. While this bug eluded us, we spent a whole lot of time seriously scrutinizing our memory management. In the end, by the time we finally nailed this bug, the entire app was completely bullet proof.

Lesson (re-) learned: if you find yourself using a raw C array, think really hard. Is that really the best way to go? We were careful, and knew what we were doing, and still paid a price. Here there be dragons.

If you want to follow me, I’m @zpasternack on Twitter and on app.net.