Category Archives: Technical

Technical information about developing applications for the iPhone SDK.

Apple WatchConnectivity

While learning Swift, one of the new features I wanted to add to the Nutrition GPA app was an Apple Watch app/extension.  Getting into WatchKit was surprisingly easy, in part because development follows a similar paradigm to UIKit interfaces but the watch user interface features are somewhat limited.

To share data between devices, Apple makes it relatively easy to pass data back and forth from the host app (on the iPhone) to the watch app.  I started with reading some framework documentation and looking at some sample code.

For my purposes, I wanted users to be able to take the Nutrition GPA daily quiz on their watch.  At first, I thought I could define “Shared Groups” to write to a common file location, but that didn’t work.  Not needing bi-directional communication, I decided to send the quiz “answers” to the phone – which would then store them to the local database.

WCSessionDelegate

The first step is defining a class to handle the watch connectivity session messages.  This class will handle various state changes and posting notifications it received data from either the watch or the phone to the rest of your app.  You’ll instantiate an instance of this class in both your AppDelegate and ExtensionDelegate classes.

class WatchSessionDelegate: NSObject, WCSessionDelegate
{
   // Unpack the data and post a notification
   func session(_ session: WCSession, didReceiveMessageData messageData: Data)
   {
      var cmd = MessagePackage(.sendMessageData, .received)
      cmd.dataPackage = EncodedData(messageData)

      DispatchQueue.main.async { NotificationCenter.default.post(.didReceiveData, cmd) }
   }

   // Unpack the data, use callback for response
   func session(_ session: WCSession, didReceiveMessageData messageData: Data, replyHandler: @escaping(Data) -> Void)
}

There are a handful of session state methods that you may want to implement specifically for your application.   They provide hooks for connectivity, reachability,  and session state.  Refer to the Apple Docs.

AppDelegate

Configuring the app delegate simply requires creating a private WatchSessionDelegate instance and setting the WCSession properties appropriately.

private var sessionDelegate = WatchSessionDelegate()

func application(_ app: UIApplication, _ launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool
{
   // Make sure WatchConnectivity is supported
   if WCSession.isSupported()
   {
      WCSession.default.delegate = sessionDelegate
      WCSession.default.activate()

      // Add a notification
      NotificationCenter.default.addObserver(
         self,
         selector: #selector(type(of: self).didReceiveData(_:)),
         name: .didReceiveData,
         object: nil)
   }
}

@objc func didReceiveData(_ notif: Notification)
{
   // unpack the notification
   guard let message = notification.object as? MessagePackage else { return }
   guard let quizData = message.dataPackage?.quizData else { return }

   // save the data
   UserData.shared().saveQuizData(quizData)
}
ExtensionDelegate

On the watch side, the ExtensionDelegate is similar.  There’s a bit more code to process and handle pending background tasks in the event connectivity disappears.

private lazy var sessionDelegate: WatchSessionDelegate = { return WatchSessionDelegate() }()

private var activeObservation: NSKeyValueObservation?

private var contentPending: NSKeyValueObservation?

private var wcBackgroundTasks = [WKWatchConnectivityRefreshBackgroundTask]()

override init()
{
   if WCSession.isSupported()
   {
      activeObservation = WCSession.default.observe(\.activationState) { _, _ in DispatchQueue.main.async {
         self.completeBackgroundTasks()
       }

      contentPending = WCSession.default.observe(\.hasContentPending) { _, _ in DispatchQueue.main.async {
         self.completeBackgroundTasks()
      }

      WCSession.default.delegate = sessionDelegate
      WCSession.default.activate()
   }
}

func completeBackgroundTasks()
{
   guard WCSession.ddefault.activationState == .activated, WCSession.default.hasContentPending == false else { return }

   wcBackgroundTasks.forEach { $0.setTaskCompletedWithSnapshot(false) }

   let date = Date(timeIntervalSinceNow: 1)
   WKExtension.shared().scheduleSnapshotRefresh(withPreferredDate: date, userInfo: nil) { error in if let error = error { 
      print("scheduledSnapshotRefresh: Error \(error)")
      }
   }
   
   wcBackgroundTasks.removeAll()
}

With the appropriate handlers added to the two application delegates, we need to consider the packaging of the shared data.   In the app delegate code (above), I’ve used a MessagePackage object without defining it.

MessagePackage & EncodedData

These two structures provide a handy transport wrapper around the actual data we want to send.  Having these classes allows us to also include some state information (sending, receiving, replied, failed) and provide some helper serialization methods through the use of dynamic properties.

In my case, I opted to use NSKeyValueEncoding/Decoding to perform the object serialization.  Key value coding is a common pattern on iOS and provides a binary Data package.

Because Swift wraps the phone and watch in unique namespaces, the class name is specific to the platform when serializing or deserializing (e.g. NutritionGPA.QuizData… and NutritionGPAExtension.QuizData…).

Early on, this was causing the data to serialize on one device and silently fail to decode on the other platform when it was received.  There are a couple ways to resolve this issue but I opted to explicitly declare each object with @objc to avoid the name changes.

@objc(QuizData) class QuizData: NSObject, NSCoding

You’ll need to do this for any custom code that the instance also serializes.

Alternatively, NSKeyedArchiver can be configured to look for specific classes and store/retrieve custom code.  It just requires setting the appropriate class name before any store/retrieve operation – which felt a little clumsy to me.  You can read about both choices on StackOverflow and decide what best works for your application.

struct MessagePackage
{
   enum Command: String {
      case sendMessageData = "SendMessageData"
   }
 
   enum Status: String {
      case sent = "Sent"
      case received = "Received"
      case replied = "Replied"
      case failed = "Failed"
   }

   var command: Command
   var status: Status
   var dataPackage: EncodedData?
   var errorMessage: String?

   init(command: Command, status: Status)
   {
      self.command = command
      self.status = status
   }
}

struct EncodedData
{
   var encodedData: Data
   var quizData: QuizData?
   {
      let optional = NSKeyedUnarchiver.unarchiveObject(with: encodedData)
      guard let quiz = optional as? QuizData else
      {
         print("Error: unable to unarchive")
         return nil
      }
      
      return quiz
   }

   init(_ encodedData: Data)
   {
      let data = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(encodedData)
      guard let dictionary = data as? [String: Any] else {
         fatalError("Failed to unarchive")
      }

      self.init(dictionary)
   }

   init(_ encodedDict: [String: Any])
   {
      guard let encodedData = encodedDict[Payload.dataKey] as? Data else 
      {
         fatalError("Error: incorrect key")
      }
      
      self.encodedData = encodedData
   }
}

Now that we have the app delegates setup and some handy wrappers around serializing our data – how do we send it?

SessionCommands & DataProvider

Both of these protocols provide some helper methods that are adopted by the classes that transmit data.

SessionCommands provides a single method that creates a MessageData object and sends it via Watch Connectivity.

DataProvider handles archiving both the raw quiz data and creating the encoded MessageData transparently to adopting classes.

Both protocols are implemented by the results view controller on the Watch app.  When the user presses “Finish”, the quiz data is encoded and sent to the phone for storage.

protocol DataProvider
{
   func messageData(_ obj: AnyObject) -> Data
}

extension DataProvider
{
   func messageData(_ obj: AnyObject) -> Data
   {
      let data = try? NSKeyedArchiver.archiveData(withRootObject: obj, requiringSecureCoding: false)
      guard let rawQuizData = data else {
         fatalError("Unable to archive")
      }
      
      let payload = try? NSKeyedArchiver.archivedData(withRootObject: [Payload.dataKey: rawQuizData], requiringSecureCoding: false)
      guard let payloadQuiz = payload else {
         fatalError("Unable to archive")
      }

      return payloadQuiz
   }
}

protocol SessionCommands
{
   func sendMessageData(_ messageData: Data)
}

extension SessionCommands
{
   func sendMessageData(_ messageData: Data)
   {
      var command = MessagePackage(command: .sendMessageData, status: .sent)
      command.dataPackage = EncodedData(messageData)

      guard WCSession.default.activationState == .activated else { return }

      WCSession.default.sendMessageData(messageData, replyHandler: { replayData in
      command.status = .replied
      command.dataPackage = EncodedData(replyData)
       }, errorHandler: { error in
         command.status = .failed
         command.errorMessage = error.localizedDescription
       })
   }
}
Debugging

There are also a number of test scenarios to consider: multiple watches paired to a single iPhone, connectivity lost (phone and watch too far apart), or handling background tasks.

Finally, it’s worth adding a bunch of unit tests around the serialization / deserialization to make sure the data can properly flow from device to device.

While it certainly helps to write unit tests to help isolate potential problems, it’s also useful to debug both the watch and phone on the simulator simultaneously.  If you’re not seeing data from one device to the other, consider debugging both app and extension at the same time in Xcode.

To do this, start debugging either the app or extension, then launch the other app on the device and select “Debug | Attach to Process” in Xcode to attach to both at the same time.

Finishing Up

I omitted some details in the code above – notably sending and handling the various notifications in both applications.

Generally, the app delegate will receive the quiz taken on the watch, then save it to the local database.  This is great – but some additional thought is required to determine how the application should respond if the user is interacting with it.  For example, if the user is on the start screen – which displays the total number of quizzes taken – the display should update to reflect new data.  Or, if the user is looking at their quiz history – the list should update to include this latest entry.

These aren’t difficult problems to solve – largely they involve having each view controller subscribe to a data notification (or redefine them in the phone’s app delegate specifically for these view controllers) and recalculating their display.

Last, there are a few areas where – now that things are working – I’d consider refactoring or a slightly more flexible approach.  Specifically, the places where “fatalError” is called create the potential for a poor end-user experience.  This code would probably be better to fail more gracefully!

Squramble Developer Blog: A Progression of Prototypes

My most recent decades have been spent writing programs on Windows computers using the C++ programming language.  The transition to programming an iPhone app on an Apple computer using Objective C was daunting.  I believe the story of how I fared might be a little interesting to other programmer types considering the same type of leap.  (And along the way I hit on one really, really useful paradigm that I think “sealed the deal” and would like to recommend to others.)

This post is “A Progression of Prototypes (or, “The-Idea-That-Sealed-The-Deal”).”

Continue reading Squramble Developer Blog: A Progression of Prototypes

Squramble Developer Blog: Word Selection for Casual Word Games

Today’s Squramble developer post talks about how one hand-crafts a word game “dictionary” for a casual word game.

Consider a game that chooses a word, mixes it up, and asks you to unscramble it as a minor diversion. What constitutes a good list of words, and where does such a list come from?

Continue reading Squramble Developer Blog: Word Selection for Casual Word Games

Squramble Developer Blog: The Story of Squramble

While building Squramble for the iPhone, I wrote  some blog-style notes that might be useful to share. While we await more “news of the world” of Looking Glass Software, I’ll be using the news feed space to do so.

This entry talks about how the idea for Squramble-The-iPhone-Game came about.

Continue reading Squramble Developer Blog: The Story of Squramble

Adding a TextField to an AlertView

Today, I was looking at how to add password protection to an app.  The basic design was the user could set a password via the options, then when they relaunched the app, would be asked to re-enter the numeric sequence.  If they got it right, the app would proceed as normal otherwise, terminate.

Ultimately, I went with the solution proposed by Jeff LaMarche on his blog.  But, many of the other solutions I found via Google appeared to use private APIs (such as addTextFieldWithValue:) which is a sure fire way to get rejected these days.

If you haven’t read the blog, or even better – his book, they are both great.  The book offers a very easy introduction to the iPhone SDK using practical examples.  It’s a great starting place for developers new to the SDK.

Managing Assets & Viewing Bundle Contents

Managing all of the assets that go into an iPhone app via Xcode can sometimes be tedious.  It seems somewhat counter intuitive to me to have the IDE manage these resources too.  Maybe I’m in the minority, but I’d prefer to just work with source code and NIBs in Xcode and keep track of the “other stuff” on my own.

The other day I was looking around for a solution to remove the non-Interface Builder assets from Xcode, organize these files into folders within my bundle, and remove the tons of Groups or Folder References from the IDE.  Happily, the answer wasn’t too bad.

Xcode Options

When you drag a resource into Xcode, it presents you with a dialog asking how to treat it:

asset_dialog

Starting from the top, the checkbox will copy the assets (and preserve the folder hierarchy, if present) while placing the resources at the root folder of the destination group.  The default radio box is to “Recursively create groups for any added folders” which is generally what you’ll want, especially if you’re using Interface Builder too.  In your bundle, these resources are placed at the “root” meaning to access them in your code, you’ll just need to specify a name.

The other radio option, “Create Folder References…”, creates a folder tree both inside Xcode and in your bundle.  However, the resources cannot be seen by Interface Builder and you’ll need to use a path if you load them at run-time.

Build Phase Copy

If you like the folder hierarchy in your bundle, or just want to move this management largely out of Xcode, it’s pretty easy to do it on your own by adding a new build phase, as shown below.  Right click on your Target and follow the cascading menus.

build_phase

In the build phase, add the following script

PBXCP=${DEVELOPER_DIR}/Library/PrivateFrameworks/DevToolsCore.framework/Resources/pbxcp
BASE=${PROJECT_DIR}/assets
DST=${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}
${PBXCP} -exclude .svn ${BASE} "${DST}"

The script calls an iPhone tool to recreate your folder structure inside your build directory before the final bundle is created.  With the script, I can remove any links or asset associations within Xcode except those I want to use in IB.  I maintain a folder “assets” off my product directory where I place any files that I’d like bundled in my app.

Checking Bundle Contents

Perhaps a trick most folks know, but I thought it was worth mentioning, is you can inspect and check the contents of your app bundle by following either navigating there via the Terminal or right clicking the app and selecting “Show Package Contents”.

I had a question about the database format of iGive Blood that shipped in version 1.0 and managed to find an old build from Apple.  For products that you’ve shipped, it’s almost the same.

  1. In iTunes, right click the app and select “Show in Finder”
  2. Copy the IPA file to your desktop
  3. Rename the extension to ZIP
  4. Double click the ZIP to create an uncompressed folder.
  5. Open the Payload folder which has the app.

It’s actually a bit disappointing that it’s so easy to get to the package contents especially as the contents aren’t obscured.  Maybe that’s a subject for another post…

Common SQLite Operations

In nearly any program with persistent data that can be manipulated, I find myself writing three types of routines: insert, update, and delete. In the example below, I focus on using SQLite to perform these operations on some generic data.

To create a simple table for demonstration purposes, we can use the command-line ‘sqlite3’ utility.  In one of my previous posts, I discuss how to automate the (re)generation of your database, but this example is contrived enough that it doesn’t warrant such attention.  In the shell, use the following commands:

   $> sqlite3 db.sqlite
   sqlite> CREATE TABLE myObject (primaryKey INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, amount NUMERIC);
   sqlite> .quit
   $>

Getting this new database into your XCode project, and adding the SQLite3 framework would be next, but I’m going to skip over it, as it’s covered elsewhere and not difficult to put together.

Here’s a simple Objective-C object as the data model for our newly created table.

MyObject.h

@interface MyObject : NSObject
{
    NSInteger primaryKey;
    NSString *name;
    float amount;
}

@property (nonatomic, assign, readonly) NSInteger primaryKey;
@property (nonatomic, retain) NSString *name;
@property (nonatomic, assign) float amount;

// Returns: new MyObject with primary key
- (id)initWithPrimaryKey:(NSInteger)pk database:(sqlite3*)db;

// Updates the database with the object values
- (void)updateDatabase:(sqlite3*)db;

// Delete an object with primary key 'pk'
+ (void)removeWithPrimaryKey:(NSInteger)pk database:(sqlite3*)db;

// Returns: primary key of newly inserted object
+ (NSInteger)addMyObjectIntoDatabase:(sqlite3*)db;

// Clean up, called on app termination
+ (void)finalizeStatements;

@end

The class defines “insert” using addMyObjectIntoDatabase:, “update” using updateDatabase:, and “delete” with removeWithPrimaryKey:database:

The class also has a initialization member presumably called by the controller that has better visibility to all of the table elements.  I’ll fill in the method bodies, but leave the actual hook-up to higher level classes as an “exercise for the reader”.

In the pattern above, generally the controller class will issue a “SELECT primaryKey FROM myObject” statement, then iterate through the returned result.  With each row, the controller factory method creates a new MyObject using the row’s primary key.  But, there are a number of valid approaches depending on your implementation requirements.

Here’s the implementation body of the MyObject class.  I took the design choice of preparing the various queries and storing the result as a class static pointer.  The advantage is performance since the queries are parameterized and “pre-compiled” for SQLite.  The disadvantage is that they consume resources that must be released at some point – in our case using the finalizeStatements method.

MyObject.m

static sqlite3_stmt *init_MyObj_statement = nil;
static sqlite3_stmt *update_MyObj_statement = nil;
static sqlite3_stmt *delete_MyObj_statement = nil;
static sqlite3_stmt *insert_MyObj_statement = nil;

@implementation MyObject

@synthesize primaryKey;
@synthesize name;
@synthesize amount;

- (id)initWithPrimaryKey:(NSInteger)pk database:(sqlite3*)db
{
   if (self = [super init])
   {
      primaryKey = pk;
      if (init_MyObj_statement == nil)
      {
         const char *sql = "SELECT * FROM myObject WHERE primaryKey=?";
         int result = sqlite3_prepare_v2(db, sql, -1, &init_MyObj_statement, NULL);
         NSAssert1(result == SQLITE_OK, @"initWithPrimaryKey: failed to prepare statement with err '%s'", sqlite3_errmsg(db));
      }

      sqlite3_bind_int(init_MyObj_statement, 1, primaryKey);

      if (sqlite3_step(init_MyObj_statement) == SQLITE_ROW)
      {
         int columnID = 1; // 0 is primary key
         char *nameStr = (char*)sqlite3_column_text(init_MyObj_statement, columnID++);
         name = (nameStr) ? [[NSString stringWithUTF8String:nameStr] retain] : @"";

         amount = sqlite3_column_double(init_MyObj_statement, columnID);
      }

      sqlite3_reset(init_MyObj_statement);
   }

   return self;
}

- (void)updateDatabase:(sqlite3*)db
{
   if (update_MyObj_statement == nil)
   {
      const char *sql = "UPDATE myObject SET name=?, amount=? WHERE primaryKey=?";
      int success = sqlite3_prepare_v2(db, sql, -1, &update_MyObj_statement, NULL);
      NSAssert1(success == SQLITE_OK, @"updateDatabase: failed to prepare with message '%s'", sqlite3_errmsg(db));
   }

   int columnID = 1;

   sqlite3_bind_text(update_MyObj_statement, columnID++, [name UTF8String], -1, SQLITE_TRANSIENT);
   sqlite3_bind_double(update_MyObj_statement, columnID++, amount);
   sqlite3_bind_int(update_MyObj_statement, columnID++, primaryKey);

   int success = sqlite3_step(update_MyObj_statement);
   NSAssert1(success == SQLITE_DONE, @"updateDatabase: update failed with message '%s'", sqlite3_errmsg(db));

   sqlite3_reset(update_MyObj_statement);
}

+ (void)removeWithPrimaryKey:(NSInteger)pk database:(sqlite3*)db
{
   if (delete_MyObj_statement == nil)
   {
      static char *sql = "DELETE FROM myObject WHERE primaryKey=?";
      int result = sqlite3_prepare_v2(db, sql, -1, &delete_MyObj_statement, NULL);
      NSAssert1(result == SQLITE_OK, @"removeWithPrimaryKey: failed with message '%s'", sqlite3_errmsg(db));
   }

   sqlite3_bind_int(delete_MyObj_statement, 1, pk);
   int success = sqlite3_step(delete_MyObj_statement);
   NSAssert1(success == SQLITE_DONE, @"removeWithPrimaryKey: failed with message '%s'", sqlite3_errmsg(db));
   sqlite3_reset(delete_MyObj_statement);
}

+ (NSInteger)addMyObjectIntoDatabase:(sqlite3*)db
{
   if (insert_MyObj_statement == nil)
   {
      static char *sql = "INSERT INTO myObject (name) VALUES ('')";
      int result = sqlite3_prepare_v2(db, sql, -1, &insert_MyObj_statement, NULL);
      NSAssert1(result == SQLITE_OK, @"addMyObjectIntoDatabase: failed to prepare statement with err '%s'", sqlite3_errmsg(db));
   }

   int success = sqlite3_step(insert_MyObj_statement);
   if (success != SQLITE_ERROR)
   {
      return sqlite3_last_insert_rowid(db);
   }

   NSAssert1(0, @"addMyObjectIntoDatabase: failed with message '%s'", sqlite3_errmsg(db));
   return -1;
}

+ (void)finalizeStatements
{
   if (init_MyObj_statement)
      sqlite3_finalize(init_MyObj_statement);
   if (update_MyObj_statement)
      sqlite3_finalize(update_MyObj_statement);
   if (delete_MyObj_statement)
      sqlite3_finalize(delete_MyObj_statement);
   if (insert_MyObj_statement)
      sqlite3_finalize(insert_MyObj_statement);
}

@end

App Analytics

One of the more daunting aspects of app development is the lack of direct contact with your customers.  With Apple serving as an intermediary, it can be difficult to know what folks like (or dislike!) about your product.  While we get a fair share of constructive feedback about our products, people occasionally post to iTunes or elsewhere online which isn’t regularly monitored.  Thus, it’s possible that we miss a really good idea or could prioritize features better based on usage.

Some make a claim for analytics as a developer tool to combat piracy but that seems less clear to me.  Knowing – even expecting – that a popular app will attract the attention of pirates should be something most developers understand and appreciate.  It’s not like the problem is unique to the App Store, nor is there a unique iPhone solution to combat it.  Piracy exists for all major computer platforms including consoles.  I’d like to think that the majority of iPhone users are honest and would rather pay a nominal amount for an app rather than jump through the required hoops (possibly making their phone less secure) to obtain something illegally.

I’ve been playing around with Pinch Media and their analytics package.  I put together a quick test app to poke around with the API and see how it performed.  It was pretty cool.  The basic process of getting it into an app was easy:

  1. Get the library.
  2. Copy the lib and header into your project.
  3. Include three frameworks: libsqlite, SystemConfiguration and CoreLocation.
  4. Start the data capture on startup.
  5. Stop the data capture on terminate.

Rather than call directly into the Pinch API, I wrote a simple singleton to wrap the interface.  I didn’t want events being triggered during development in the simulator, or in the event we want to add an app option/preference to “opt in”.

The implementation file is a bit “loose” – especially the handling of the static singleton which I’d probably cleanup if attempting to productize this code.  However, for experimentation purposes, the wrapper was a snap and looks something like this:

Analytics.h


#import <Foundation/Foundation.h>
@class CLLocation;

@interface Analytics : NSObject
{
}

+ (id)initAnalytics:(NSString*)appCode useCoreLocation:(BOOL)coreLocation useOnlyWiFi:(BOOL)wiFi;
+ (void)endAnalytics;
+ (id)shared;
- (void)startSubWithName:(NSString*)name timeSession:(BOOL)timed;
- (void)endSubWithName:(NSString*)name;
- (void)setLocation:(CLLocation*)location;

@end

Analytics.m


#import "Analytics.h"
#import "Beacon.h"

#if (!TARGET_IPHONE_SIMULATOR)
static Analytics *obj_Analytics = nil;
#endif

@implementation Analytics

#if (!TARGET_IPHONE_SIMULATOR)

- (id)init
{
   self = [super init];
   obj_Analytics = self;
   return obj_Analytics;
}

+ (id)initAnalytics:(NSString*)appCode useCoreLocation:(BOOL)coreLocation useOnlyWiFi:(BOOL)wiFi
{
   return [Beacon initAndStartBeaconWithApplicationCode:appCode useCoreLocation:coreLocation useOnlyWiFi:wiFi];
}

+ (void)endAnalytics
{
   [obj_Analytics release];
   obj_Analytics = nil;

   [[Beacon shared] endBeacon];
}

+ (id)shared
{
   if (obj_Analytics == nil)
   {
      obj_Analytics = [[self alloc] init];
   }

   return obj_Analytics;
}

- (void)startSubWithName:(NSString*)name timeSession:(BOOL)timed
{
   [[Beacon shared] startSubBeaconWithName:name timeSession:timed];
}

- (void)endSubWithName:(NSString*)name
{
   [[Beacon shared] endSubBeaconWithName:name];
}

- (void)setLocation:(CLLocation*)location
{
   [[Beacon shared] setLocation:location];
}

#else

+ (id)initAnalytics:(NSString*)appCode useCoreLocation:(BOOL)coreLocation useOnlyWiFi:(BOOL)wiFi { return nil; }
+ (void)endAnalytics {}
+ (id)shared { return nil;}
- (void)startSubWithName:(NSString*)name timeSession:(BOOL)timed {}
- (void)endSubWithName:(NSString*)name {}
- (void)setLocation:(CLLocation*)location {}

#endif

@end

After hooking in the startup/shutdown code in your app delegate, the idea is you scatter “markers” throughout your app to track or even time various events using the startSubWithName:timeSession method.

To use the API, you’ll need to register with the Pinch Media site to obtain a unique app-id that will aggregate various statistics on their server.  The library collects a number of stats by default including:

  1. OS version
  2. Device type
  3. Demographic information (if known)
  4. Regional usage (if using CoreLocation)
  5. Lifecycle data (app version, time used, number of actions, etc.)

And – all the various actions you scattered too.  The default charts on the web site are good but basic.  You can export the data to CSV or access it online via a nifty API.

While running a test over a few minutes, I noticed the library (r69) leaked a small amount of memory during the asynchronous send.  During the transfer, I didn’t detect a noticeable performance impact but then, the test app wasn’t exactly doing real time calculations either.  Instruments reported about 1 kb in the library thread.  The library appears to periodically send data perhaps after a certain time period or once the event queue fills.    The data is also sent when the app terminates – which apparently can cause Springboard to “force quit” your app and lead to a misleading crash report.

Viewing the data online took a while.  It was about 24 hours for the test data to show up on the website, and nearly 48 hours for the custom events to appear.

Linking with the library (and the other frameworks) added about 200 kb to the binary, although that may be slightly misleading as libsqlite3 was already being linked into the tester app.

I’m glad that the library also offered a feature to enable developers to expose “opt in” features as users can sometimes be sensitive to profiling, even if it’s anonymous.  Before hooking up the service to your app, it’d be worth checking out their privacy policy and making sure you’re not passing user specific data (which Pinch discourages and won’t allow) to their site.

If you’re looking for some insight into what your customers are doing with your app, linking in Pinch Media’s analytics library may be the answer.  From an ease of integration point of view, it was definitely very easy and worked as advertised.

Adding Build Versions via Xcode

One of the things I was keen on setting up early in development was the automatic injection of a build number for our products.  Having a build number is vital when handing out early “pre release” versions of the app to testers for review.  During development, there are likely to be a number of builds and release candidates – and you’ll need a way to differentiate between them.

It turns out I wasn’t alone as a quick search on Google provided a wealth of possible options.

AGVTOOL

Apple provides a generic versioning build tool.  It provides a number of useful functions, like hooking directly into CVS or Subversion to commit modified code.  In addition, it automatically creates a CFBundleVersion key in your info.plist file.

By default, CFBundleVersion is 1.0 in the info.plist file.  But, there’s another key, CFBundleShortVersionString which can be used for this same purpose.  Thus, you could set the short version to “1.0” and let the agvtool set the CFBundleVersion key to the current build number.

If you automate your builds on a neutral machine, this technique is definitely something to consider.  It seems the most natural fit for generating a build number that can be referenced in an app.  There are a number of other folks who have tutorials and walkthroughs for exactly how to set this tool up for use in development:

Inner Exception

Chris Hanson’s Blog

Jamie Montgomerie’s Blog

SCRIPTING

While the agvtool has some definite benefits, it felt a bit “manual” to me.  Sure, you can hook it up as a post-build script or on a separate box that does nightly builds (or whatever) but I was looking for something else – like the revision number instead of a build number, per say, that was always updated without a ton of extra work.

Using the revision number from Subversion seemed an easier way to link the code state with the build number without the extra hassle of tagging or branching specifically for the purpose of sharing a build with testers or external stakeholders.  So, I turned my attention to script based solutions.

Luckily, there are a number of posts that describe how to do the exact same thing with a variety of scripting tools and languages.  Most hinge on creating a custom post-build step, then running either some Perl, Ruby, or shell script to substitute in the desired number into a plist file associated with your project.

Another choice in scripting tools is “PlistBuddy” which can read/write values to plists – but you’d need to wrap it in a shell script and at that point it’s debatable whether using Perl (or the shell) to perform the insertion isn’t just as easy.  PlistBuddy doesn’t solve the potential problem of parsing the revision number from Subversion either.

Adding something like this into your Xcode is trivial.  Under “Groups & Files” expand the Targets section and right-click the target to select Add –> New Build Phase –> New Run Script Phase.  A window will pop up where you can insert your own commands into the overall build.  Here’s some sample code from other folks:

Ruby – Jeff LaMarche, author of “Beginning iPhone Development.”

Git – Marcus Zarra.

Perl – Stackoverflow.

In the end, I took the route proposed by Daniel Jalkut on Red Sweater using Subversion with a couple minor modifications:

# Xcode auto-versioning script for Subversion
# by Axel Andersson, modified by Daniel Jalkut to add
# “–revision HEAD” to the svn info line, which allows
# the latest revision to always be used.

use strict;

die “$0: Must be run from Xcode” unless $ENV{“BUILT_PRODUCTS_DIR”};

# Get the current subversion revision number and use it to set the CFBundleVersion value
my $REV = `/usr/local/bin/svnversion -n ./`;
my $INFO = “$ENV{BUILT_PRODUCTS_DIR}/$ENV{WRAPPER_NAME}/version.plist”;

my $version = $REV;

# (Match the last group of digits and optional letter M/S):
($version =~ m/\d+[MS]*$/) && ($version = $&);

die “$0: No Subversion revision found” unless $version;

open(FH, “$INFO”) or die “$0: $INFO: $!”;
my $info = join(“”, <FH>);
close(FH);

$info =~ s/r0000/r$version/;
print $version;
print $INFO;

open(FH, “>$INFO”) or die “$0: $INFO: $!”;
print FH $info;
close(FH);

The major change from the original is that I write to a separate plist file (version) instead of Info.plist with a default key of “r0000” which is used for the substitution.  Really there’s not much difference and the choice of which scripting language is largely a personal choice for maintenance sake.