TaskLog 1.2 app update metrics

Another app update, another metrics post.

I just shipped TaskLog 1.2. This was a pretty major feature update – the largest in TaskLog’s history. As with the recent PuzzleTiles update, I thought I’d share some metrics about the app, and the update.

The previous shipping version of TaskLog comprised 3,388 lines of Objective-C code in 28 files (121 lines per file), plus 226 lines in C header files (8 lines per file). This is physical lines of code (SLOC), as counted by cloc.

The new updated version comprises 4,649 lines of Objective-C in 37 files (125 lines per file), plus 226 lines in 36 header files (8 lines per file).

Objective-C C Headers Total
TaskLog 1.1.2 28 files, 3,388 lines 26 files, 226 lines 54 files, 3,614 lines
TaskLog 2.0 37 files, 4,649 lines 36 files, 290 lines 73 files, 4,939 lines

The largest file was (and still is) the main view controller, TTMainViewController.m, which was previously 889 lines of code, and in the latest update is 1,085 lines of code.

In total, I spent 112.5 hours developing this update. Had I been working on it full time, I could have finished it in 3 weeks. Doing it part time, it ended up more like 6 weeks.

Notably (unlike the PuzzleTiles update), I added no Swift to TaskLog. It wasn’t really a conscious decision. I just did my thing, and when it was done, well, there was no Swift in there. I guess it didn’t really occur to me. TaskLog is several generations of code newer than PuzzleTiles, so it needed much less of an overhaul; maybe that was it.

On a semi-related note, I recently converted PuzzleTiles to Swift 2.0, and it took me two hours to straighten out all the code and make everything build and run properly once the Swift converter was done. Considering it has less than 600 lines of Swift code, that seems like a whole lot of effort. I’ve been working on an (as yet non-shipping) app for quite some time which is currently over 20,000 lines of Objective-C. It occurs to me that if it were written in Swift, I’d be in a world of hurt right now. I wonder if Apple isn’t updating Swift too, um, swiftly.

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.

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.

Be careful with weak references

I’m working on an update to VideoBuffet. One of the changes is that movies are loaded on a background thread, to make the main UI responsive during loading. So I’ve a block of code in one of my view controllers that looks like this:

- (void) setCurrentPathURLs:(NSArray*)URLs
{
    [self moviesStartedLoading];

    __weak id weakSelf = self;
    [self.moviesDoc setMovieURLs:URLs completion:^{
        [weakSelf moviesFinishedLoading];
    }];
}

The little dance with a __weak reference is to avoid a retain cycle. The controller has a strong reference to moviesDoc, moviesDoc has a strong reference to the block, and because the block references self, it will have a strong reference to it. By making a weak reference to self, it won’t be retained by the block, and the cycle is broken.

I thought this was all good, until I did some sanity testing on 10.7 Lion, and discovered that it simply crashed when trying to open a movie. It didn’t take long to trace it back to the above code. Then I remembered this. Whoops, I’m making a weak reference to an NSViewController. Turns out that’s fine in 10.8, but not 10.7, according to the Transitioning to ARC Release Notes:

Note: In addition, in OS X v10.7, you cannot create weak references to
instances of NSFontManager, NSFontPanel, NSImage, NSTableCellView,
NSViewController, NSWindow, and NSWindowController. In addition, in
OS X v10.7 no classes in the AV Foundation framework support weak references.

The solution is to simply make it __unsafe_unretained, as in:

- (void) setCurrentPathURLs:(NSArray*)URLs
{
    [self moviesStartedLoading];

    __unsafe_unretained id weakSelf = self;
    [self.moviesDoc setMovieURLs:URLs completion:^{
        [weakSelf moviesFinishedLoading];
    }];
}

Really, though, the moral of the story is: test your app, on all OS’es it supports. If we hadn’t gone back and tested with 10.7, we’d never have discovered this, and we’d have ended up with many 1 star reviews.