Friday, December 23, 2011

Cocoaheads Talk: Developing an iPad in-store interactive product browser

Earlier in the year I was hired to build an interactive product browsing iPad app for a large clothing retailer. The app was deployed in brick & mortar stores, kiosk-style. It ended up being a technically interesting project, requiring simulation of bubble physics and careful optimisation to be able to quickly scrub full iPad screen resolution photographs of the modelled clothing, with panning and zooming up to 4x resolution. In total, 840 high resolution (4x) images were embedded.


In April 2011 I gave a talk at Melbourne Cocoaheads about the project, detailing some of the technical challenges and how I solved them. You can watch this talk on Vimeo (embedded below).


Proudly, the app won a FWA Mobile Of The Day (MOTD) Award in October.

Tuesday, July 12, 2011

Core Data debugging with SQLite

Core Data is designed to be a black box. Under the hood on iOS it (normally) uses a SQLite database for data persistance. The use of SQLite is an implementation detail and not one we are encouraged to worry about. However, in practice this detail can be very convenient and accessing the SQLite database directly can be a handy tool for debugging & testing. This post will describe how to do that.


Sample App


All the example code in this post can be found in a demo app, introduced in the previous post, Organising Core Data for iOS.


Locating the Store


In the app delegate class look for the method that creates the NSPersistentStoreCoordinator. Apple's template has it in the persistentStoreCoordinator method.  This is where the store path is passed to the NSPersistentStoreCoordinator (as a URL). Add an NSLog() statement to output the path to the console.


For example, here is a snapshot of the persistentStoreCoordinator method as generated by Apple's template; I added the NSLog() line:


- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
    if (__persistentStoreCoordinator != nil)
    {
        return __persistentStoreCoordinator;
    }
    
    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"OrganisingCoreData.sqlite"];
    NSLog(@"Core Data store path = \"%@\"", [storeURL path]);

    NSError *error = nil;
    __persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    if (![__persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error])
    {
    ...

In the simulator you'll get output something like:


2011-07-01 22:13:25.971 OrganisingCoreData[14217:207] Core Data store path = "/Users/chris/Library/Application Support/iPhone Simulator/4.3.2/Applications/22CD429E-ADD2-4AAA-9C9E-5E57828A6FF8/Documents/OrganisingCoreData.sqlite"


SQLite


Now you've got the Core Data store path, you can hand it to a SQLite client and poke around. Grab a GUI SQLite client if you like, but I recommend the command-line client as it is built-in and easy to use. Open Terminal and run "sqlite3" pasting in the store path as argument.


$ sqlite3 "/Users/chris/Library/Application Support/iPhone Simulator/4.3.2/Applications/22CD429E-ADD2-4AAA-9C9E-5E57828A6FF8/Documents/OrganisingCoreData.sqlite"
-- Loading resources from /Users/chris/.sqliterc
SQLite version 3.6.12
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .tables
ZDVD          ZPERSON       Z_METADATA    Z_PRIMARYKEY


You can view all the database tables with the ".tables" command. You will see each Core Data entity represented by a table, named after the entity but prefixed with "Z" and all caps. My example app contains a Core Data model with two entities, DVD and Person (see model layout image below). In SQLite we see a table for each entity, "ZDVD" and "ZPERSON". You will also see two extra tables, "Z_METADATA" and "Z_PRIMARYKEY". These are used by Core Data for administration.


You can examine the schema of each table with the ".schema" command. SQLite shows the "CREATE" command for each. This can be useful to see the types that Core Data picks for each field as well as how any indexes are configured.


sqlite> .schema ZPERSON
CREATE TABLE ZPERSON ( Z_PK INTEGER PRIMARY KEY, Z_ENT INTEGER, Z_OPT INTEGER, ZUSERNAME VARCHAR, ZNAME VARCHAR );
CREATE INDEX ZPERSON_ZUSERNAME_INDEX ON ZPERSON (ZUSERNAME);
sqlite> .schema ZDVD
CREATE TABLE ZDVD ( Z_PK INTEGER PRIMARY KEY, Z_ENT INTEGER, Z_OPT INTEGER, ZOWNER INTEGER, ZPURCHASEDATE TIMESTAMP, ZTITLE VARCHAR );
CREATE INDEX ZDVD_ZOWNER_INDEX ON ZDVD (ZOWNER);


Tables contain a column for each entity attribute, names based on the attribute names, all caps and prefixed with "Z". Some extra administrative tables are also present, each prefixed with "Z_". You may also notice that all tables automatically get a primary key as "Z_PK".


Any one-to-one or one-to-many relationships are tracked in a column, like "ZOWNER" in the example. Many-to-many relationships are tracked in separate tables, automatically managed by Core Data.


You can query the data using all the usual SQL commands.

sqlite> SELECT * FROM ZPERSON;
Z_PK        Z_ENT       Z_OPT       ZUSERNAME   ZNAME      
----------  ----------  ----------  ----------  -----------
1           2           3           chris       Chris Miles
sqlite> SELECT ZUSERNAME,ZPURCHASEDATE,ZTITLE FROM ZDVD LEFT JOIN ZPERSON ON ZPERSON.Z_PK=ZDVD.ZOWNER WHERE ZUSERNAME='chris';
ZUSERNAME   ZPURCHASEDATE  ZTITLE      
----------  -------------  ------------
chris       279979769      The Hangover
chris       -61041031      Terminator 2


You can also modify the data, if care is taken not to invalidate any of the Core Data administrative fields, or create broken relationships or other broken states.


Recently I added a weekly progress graph to an iPhone project and part of the testing required data that was generated over the period of many weeks. Waiting a few weeks until real data was collected was out of the question, so I simulated accelerated use by repeatedly advancing the date field values by a few days at a time and restarting the app each time.


For example, in my demo app I set a purchased date of 2009-11-15 for "The Hangover", which is stored as 279979769 (seconds since a reference date).  By accessing the Core Data store directly I can advance this date by one week with the SQL query:


sqlite> UPDATE ZDVD SET ZPURCHASEDATE=ZPURCHASEDATE+(60*60*24*7) WHERE Z_PK=1;


After relaunching the app the purchased date is now shown as "2009-11-22".


Core Data Faulting


In my example above I mentioned relaunching the app after modifying the SQLite store directly. Modifying the store while the app is not running is the safest way to ensure changes will persist. However, it is not always necessary if you understand how and when Core Data reads and writes to the store.


Core Data tracks entity objects in memory but lazy loads the actual attribute values. The act of lazy loading is called faulting. If you modify an attribute value directly in the store and then a fault is fired for the corresponding managed object, the changed values will be loaded into memory. Managed objects will then persist in memory until no longer needed. Core Data assumes it has control of the store and will not attempt to load the managed object attribute values again until the object is invalidated and another fault is fired for it. So if you change a value after a managed object has already been populated for the record, the changes won't immediately appear. You will need to understand your managed object behaviour and lifetimes to work out when to expect the changes to be represented in memory.


Also be cautious not to make changes directly to the store while an app is running and then interact with the app in a way that commits changes to the database. If Core Data needs to update the record that you had modified, it will assume (rightly so under normal conditions) that it has exclusive control and will overwrite your sneaky changes with data from the in-memory managed objects.


Device Testing


Accessing the Core Data SQLite store directly is easiest when developing in the Simulator, as demonstrated above. However, it is still possible to access the store file on the device. The simplest way is to enable iTunes file sharing for the app. Then you can copy the sqlite file out to the desktop to interact with directly. You can also modify it and copy it back to the device.


Enable iTunes file sharing for the app by editing the Info.plist for the app and adding "Application supports iTunes file sharing" (aka UIFileSharingEnabled) with a boolean value of YES. Re-install the app and then connect to iTunes to get access to SQLite store file.


Summary


The use of SQLite as an underlying data store for Core Data is an implementation detail that can be used to our advantage when debugging and testing Core Data driven iOS apps.


Related


Also see:

Monday, June 27, 2011

Organising Core Data for iOS

I rarely work on an iOS project that doesn't use Core Data for persistent data management. Over time I have settled on a strategy for organising Core Data and its Managed Object classes, which I will describe in this post. This information will hopefully be useful to those new to Core Data wondering how to organise things, as well as those more experienced interested in how other iOS devs work with Core Data.


I have created a little demo iPhone app that goes along with this post.  You can get it from github. The app demonstrates how I organise and use Core Data in a real-world (although very simplified) app and demonstrates how to wire Core Data to table views, including automatically updating table content (with animation) when Core Data changes occur.


Model Layout


Let's start by defining a very simple model. Assume we are building an app to track people's DVD collections (remember DVDs? If you are like me you ended up with a big pile of them gathering dust in a cupboard somewhere). We will define two entities, "Person" and "DVD", and link them together.



Each person may own many DVDs so there is a one-to-many relationship between Person and DVD. The "username", "name" and "title" attributes are strings and "purchaseDate" is of type Date.


NSManagedObject Subclasses


After laying out the model I then select each Entity and customise the Class name. This field sets the name of the Managed Object (NSManagedObject) subclass that will be tied to the Entity.  I have standardised on using the Entity name prefixed with "MO". So for Person I set a Class name of "MOPerson" and for "DVD" I set "MODVD".


Actual code for the NSManagedObject subclasses are created by selecting the Entities and choosing the "Editor / Create NSManagedObject Subclass..." menu item.




I organise all my model code into a "Model" group and place the auto created NSManagedObject subclasses in a "Managed Objects" sub group. Note that after creating the NSManagedObject subclass files I never touch them. I consider them to be "owned" by Core Data. The main advantage of this strategy is that I can change the model layout and re-generate the NSManagedObject subclasses at any time during development, without risk of losing customisations.


Model Interface


My general rule of thumb is to encapsulate all the Core Data queries in the model code. The rest of the project accesses the model via this interface and generally doesn't talk to Core Data directly (other than working with the Managed Object subclasses). This helps keep the "M" separate from the "V" & "C".


So, if I don't touch the NSManagedObject subclass files how do I customise the interface?  I use our good friend the Objective-C Category. I define a "Management" category for each Managed Object that provides the API for the rest of the project to work with, including inserting, fetching and deleting records from the database.


Furthermore, to keep the Management category methods as short and simple as possible (aiming for DRY) I define a bunch of helper functions containing the common Core Data query logic. See ModelUtil.h/ModelUtil.m for these convenience functions.  You'll see that they centralise access to the "default" Managed Object Context, which is the NSManagedObjectContext on the main thread created by the application delegate if you use Apple's application templates.  They are mostly very simple functions, but are reusable across projects and reduce repetition.
With all that in place the NSManagedObject category methods are written specifically for the application needs. For the demo project MOPerson (Management) contains class methods to insert and retrieve MOPerson instances. The implementation looks like:


static NSString *entityName = @"Person";

@implementation MOPerson ( Management )

+ (MOPerson *)insertPersonWithUsername:(NSString *)username
                                  name:(NSString *)name
                  managedObjectContext:(NSManagedObjectContext *)moc
{
    MOPerson *person = (MOPerson *)[NSEntityDescription
                                    insertNewObjectForEntityForName:entityName
                                    inManagedObjectContext:moc];
    person.username = username;
    person.name = name;
    
    return person;
}

+ (MOPerson *)insertPersonWithUsername:(NSString *)username
                                  name:(NSString *)name
{
    NSManagedObjectContext *moc = defaultManagedObjectContext();
    return [MOPerson insertPersonWithUsername:username
                                         name:name
                         managedObjectContext:moc];
}

+ (MOPerson *)personWithUsername:(NSString *)username
{
    NSPredicate *predicate = [NSPredicate
                              predicateWithFormat:@"username == %@",
                              username];
    
    NSArray *sortDescriptors = [NSArray arrayWithObject:
                                [NSSortDescriptor sortDescriptorWithKey:@"username"
                                                              ascending:YES]];
    
    MOPerson *person = (MOPerson *)fetchManagedObject(entityName,
                                                      predicate,
                                                      sortDescriptors,
                                                      defaultManagedObjectContext());
    
    return person;
}

@end

In our application code, fetching the MOPerson instance for a particular username is as simple as:

MOPerson *person = [MOPerson personWithUsername:username];

which will return nil if not found.


Inserting a new Person instance in the database is one line followed by a request to the Managed Object Context to commit all changes:

MOPerson *person = [MOPerson insertPersonWithUsername:username name:name];

if (!commitDefaultMOC()) {
    // handle error
}

In a more interesting project the Management categories may also contain instance methods to customise attribute access or validation, for example.


To see a working example of all this get the demo app source code.


Summary


This is how I organise Core Data in my iOS projects. None of this is rocket science but standardising on a strategy for organising Core Data will help keep your projects tidy and maintainable. I am interested to see how other iOS devs organise Core Data in their projects so I can continue to improve my practice. In future posts I will share some more of my experiences with Core Data on iOS.

Tuesday, June 21, 2011

Locayta Notes 2.1

Locayta Notes on iPhone
One of my projects for Locayta Limited has been the development of Locayta Notes for iPhone & iPad. We just launched another big update to the free notes app that already includes full text search and Dropbox syncing. The new update, version 2.1, adds many enhancements across the board, including improvements to how notes are synced to Dropbox:
  • Better default note filenames ("Note 2011-06-21.txt");
  • Note filename renaming;
  • The Dropbox folder to sync notes with can be selected and changed at any time;
These enhancements help improve Locayta Notes as a general text editor for files within Dropbox. Notes are synced to Dropbox as plain text files, meaning they can be easily viewed and edited on a desktop computer or other device. Plain text files added to the Dropbox folder from any device will automatically show up in Locayta Notes as an editable note.


We also added:

  • Notes can be sorted by date, title or filename;
  • Background colour can be customised per note;
  • Matched search terms quick jump in iPhone UI (was already in iPad);
  • Improvements to syncing logic and non UTF-8 character set handling.

Locayta Notes started out as a simple showcase app for Locayta Search Mobile, a port of Locayta's full text search library to the iOS platform (and one of my other projects). Since then it has grown to become a fully featured productivity app in its own right.


Locayta Notes on iPad


Locayta Notes is free in the App Store as a universal app for iPad/iPhone/iPod Touch.



Monday, May 23, 2011

CMPopTipView - new animation option

In December I introduced a custom iOS UIView: CMPopTipView, a view for presenting targeted tips or callout views.


CMPopTipView got a small update recently, a new presentation animation style submitted by myell0w.


The default animation style I originally implemented was a simple slide into place while fading in effect. Subtle, nice. This is still the default.


The new animation style option is a pop out style animation, like iOS UIAlertViews. This style could be useful when you need to catch the user's attention.


To enable the pop out style animation set the animation property to CMPopTipAnimationPop. For example:

CMPopTipView *popTipView = [[[CMPopTipView alloc] initWithMessage:@"My message"] autorelease];
popTipView.animation = CMPopTipAnimationPop;
popTipView.delegate = self;
[popTipView presentPointingAtView:button inView:self.view animated:YES];

Get CMPopTipView from github or read more details about this update from the pull request.

Monday, May 16, 2011

River Level 1.1



I recently updated River Level for iPhone to version 1.1. The main feature added to 1.1 is the ability to customise the units of measurement. Specifically:
  • River depth: meters or feet;
  • Water temperature: celsius or fahrenheit
River Level was created in Australia, initially for NSW waterways, so had only shown measurements in meters & celsius. After the addition of USA rivers boosted the app's popularity in America the option for feet/fahrenheit was requested by a lot of American users (understandably) so I was happy to find some time to add the option.


Units of measurement can be customised by going to system Settings / River Level.


To make the user experience even smoother, the first river you add determines the default units of measurement. So if the first river you add is an American river, the units automatically default to feet/fahrenheit. This should remove the need for most users to access Settings.



River Level is free in the App Store.

Thursday, April 7, 2011

Swap It Don't Stop It iPhone app

My recent handy work is out in the wild in the form of the Swap It Don't Stop It iPhone app for a new Australian Government health campaign.


The Swap It Don't Stop It campaign is all about educating and encouraging people to swap unhealthy food and unproductive activities for those that can improve your overall health & fitness.


The iPhone app provides a tool for tracking your "swaps", gaining achievements, receiving alerts to be reminded when to swap, tracking food swaps (with an integrated shopping list) and finding healthy activities near you.


Technically, the app required:

  • Custom views to match the attractive design;
  • Core animation for some custom transitions and UI elements;
  • Server side REST API integration;
  • MapKit and Location Services along with SOAP API integration for finding activities by location;
  • Core Data for local data management;
  • Push notifications for alerts;
  • Retina display optimisation.

The app looks really great and is free on the Australian App Store. I was very happy with how it turned out. I developed the app on behalf of the great guys at Millipede Creative Development and JWT Melbourne.


Monday, April 4, 2011

iOS Dev, Beta & Production builds installed side-by-side

A development and testing pattern I adopted early on for my iOS projects was to configure projects such that development, ad hoc test and release builds could all be installed on my device at the same time. This allows for side-by-side testing and easy comparison of the builds, each being at different states of completion at any point in time.


For example, this iPhone screen snapshot demonstrates dev, beta & release builds of my hobby project River Level installed side-by-side.


Dev, beta & release builds of River Level installed side-by-side
iOS differentiates apps by their Bundle Identifiers (CFBundleIdentifier) which are defined in the Project-Info.plist.  A Bundle Identifier looks like a reverse DNS address, e.g. "com.apple.safari". What I like to do is to set the Bundle Identifier to different values for each build configuration, thus allowing each to be installed separately. I don't do this manually, though, as I'll explain below.


Note that some iOS features – like Push Notifications, Game Center and In-App Purchase – require the Bundle Identifier to match the value configured in iTunes Connect so you can test these features. In those cases this technique won't be effective, unless you set up multiple app instances in iTunes Connect for each build configuration.


The other parameter I customise per build configuration is the Bundle Display Name (CFBundleDisplayName). This is the app name as displayed on the home screen. iOS doesn't require these names to be unique, so if you changed the Bundle Identifiers without changing the Display Names you would end up with multiple instances of your app all with the same name and icon, making it difficult to work out which build is which.


My convention is to append "∆" to dev builds and "ß" to ad hoc builds.


To customise the Bundle Identifier and Bundle Display Name per build the process is:

  • Append ${BUNDLE_ID_SUFFIX} to the "Bundle identifier" in Project-Info.plist.
  • Append ${BUNDLE_DISPLAY_NAME_SUFFIX} to the "Bundle display name" in Project-Info.plist.
Custom variables appended to Bundle Display Name and Bundle Identifier values



Note that with no other changes these variables will be expanded to empty strings and so will have no effect. This is how I handle Release builds, so they are submitted to the App Store with standard values.


The next step is to define the custom values for each build configuration.  My convention is:


Development (Debug) builds:
  BUNDLE_DISPLAY_NAME_SUFFIX=∆
  BUNDLE_ID_SUFFIX=.dev


Ad Hoc builds:
  BUNDLE_DISPLAY_NAME_SUFFIX=ß
  BUNDLE_ID_SUFFIX=.adhoc

Release builds:
  Neither of these settings is defined.


The place to put these settings is in each relevant Build Configuration of the Project. Note: some people edit the build configuration for their Target, but I recommend only doing this if you need a setting specifically for a build target.  If you only have one target, edit the build settings for the whole Project.  That way, if you add Targets later they will inherit the Project build settings.


To add custom settings to build configurations, in Xcode 3:

  • Select menu "Project" / "Edit Project Settings"
  • Select "Build" tab
  • Select a build configuration in the Configuration list (e.g. "Debug")
  • Select "Add User-Defined Setting" from the command list (bottom left corner pulldown)
  • Add a user-defined setting for each of "BUNDLE_DISPLAY_NAME_SUFFIX" and "BUNDLE_ID_SUFFIX" with the values you choose.
  • Repeat this for each build configuration that you want to install separately to the others.

User-defined settings for a Debug build configuration
In Xcode 4, the process is similar, although the UI has changed.  Select your project; select the "Build Settings" tab, then select "Add Build Setting"/"Add User-Defined Setting". You can set the values for Debug and Ad Hoc at the same time with the new UI.


To see any user-defined settings, scroll the build settings table to the bottom; or use the "Show" list to filter only "User-Defined Settings" (Xcode 3).


To delete a user-defined setting, highlight the setting and hit the delete key.


That's it. Now re-build and install and the app should appear separately to the other builds. Feel free to install as many different builds as you feel like.

Tuesday, March 8, 2011

iRaspberry 1.6

Recently I had the pleasure of working with the Lycette Bros, hired to update their cool iRaspberry iPhone apps (App Store: iRaspberry Lite [free], iRaspberry Pro [AU$1.19]).


iRaspberry is a simple but fun concept that has your iPhone stand-in for your mouth expressions! It comes with a load of video clips of various mouths performing raspberries, tongue pokes, kisses and other actions and you simply choose an action and hold it in front of your mouth to really show people how you feel about them.




I helped the Lycette Bros produce the 1.6 release, which added a bunch of new video clips, updated the video handling & in-app purchase code for iOS SDK 4.x and added iAds to the free lite version.


iRaspberry is also available on Android (Android Market: iRaspberry Lite [free], iRaspberry Pro [AU$1.00]).



Friday, March 4, 2011

git sharing with dropbox

I recently posted about using Dropbox as a git server: git push dropbox.  I have since refined this process as I became more familiar with git and fine tuned my development workflow. So here's an update on how I now use Dropbox to both share and backup my git repositories.


(note: in the examples below "$" is the bash prompt; all commands are entered in the OS X Terminal. I'm very much an old school UNIX command-line guy! However, on other platforms, all commands will be possible from your favourite git client.)

Initialise a git repository for Project, if one does not already exist:


  $ cd Project
  $ git init .
  $ git commit


Create a git server repository in Dropbox.  It is initialised as "bare" which means it won't hold a working copy of the repository. It is a shared repository only.
  
  $ mkdir ~/Dropbox/git-server/Project.git
  $ git init --bare  ~/Dropbox/git-server/Project.git

In the local repository, add the Dropbox repository as the remote origin then push the current branch up to it.

  $ git remote add origin ~/Dropbox/git-server/Project.git
  $ git push origin master

That's all there is to sharing and backing up your project repository.  Whenever you need to (I generally do it after every commit) you can "git push origin master" again to update the Dropbox repository.

On another computer, to clone a copy of the project repository from Dropbox:

  $ git clone ~/Dropbox/git-server/Project.git
  $ cd Project
  $ git status

To update a clone from the Dropbox server:


  $ git pull origin master


Too easy!

Some other useful commands for examining the remote repositories:

  $ git remote
  $ git remote show origin


And note that you can add other remote repositories.  So you could add a github repository so you can selectively push stable snapshots of your project to github for public consumption, while keeping work-in-progress commits stored locally and backed up in Dropbox.

Thursday, February 24, 2011

Locayta Search Mobile for iOS beta5

At Locayta we recently released beta5 of the Locayta Search Mobile full text search library for iOS.  This may be the last beta as we prepare for the 1.0 release soon.


Beta5 included the usual bugfixes and improvements to memory usage.  It also included a new feature that assists with search results display. The feature allows you to fetch the character ranges of all terms in a text field that were matched by a search result.


This feature isn't as simple as it sounds, due to the search engine's ability to infer relevant document terms by spell correcting, stemming or synonym expansion of query terms.  This means that terms matched in a document aren't necessarily the same as those in a query. The new character range fetching exposes this information so you can clearly highlight the term "test" in a document that matched the search term "testing", for example.


Here are some more examples.  Assume a note contains the line of text:

These notes are just for testing.

Each of these search queries would match this note by the highlighted terms:

  • "test note" : These notes are just for testing.
  • "test notr" : These notes are just for testing.
  • "I am not testing" : These notes are just for testing.
i.e. the same document terms were matched by each query, however in some cases stemming or spelling correction was required to get the match.


Another example: consider a note containing this line of text:

My Christmas list

With an explicit synonym defined to expand "xmas" as "christmas", a search for "xmas" would match the document on the term "christmas":
  • "xmas" : My Christmas list



Using the character range fetching feature for matching search terms gives you the information needed to highlight or find search terms in your documents.  This is demonstrated in Locayta Notes 2.0 (free in the App Store).  The screenshots above are from Locayta Notes, demonstrating the examples.





Thursday, February 10, 2011

Locayta Notes 2.0

Locayta Notes on iPad
One of my primary projects for Locayta has been the development of the Locayta Search Mobile full text search library for iOS.  As part of that work I built a note taking / text editing app for iPad/iPhone that helped us show off embedded full text search on mobile devices. Recently I got the chance to add some significant functional enhancements to this app, which has now been released: Locayta Notes 2.0.


The biggest feature in Locayta Notes 2.0 is integration with Dropbox which will automatically sync all your notes across all your iOS devices.  Notes will also be accessible as plain text files in Dropbox on your linked computers. This feature has been extremely handy for me personally, which made Locayta Notes become my primary note taking app on my iPad & iPhone.


But that's not all, here's the full list of features in Locayta Notes 2.0:


 • Instantly search the contents of all notes using Locayta's embedded full text search engine, Locayta Search Mobile. Includes smart search features such as automatic spell correction, synonyms, stemming and context snippets.


 • Dropbox syncing to automatically sync notes between all your iPads, iPhones and computers.


Locayta Notes on iPhone
 • Customise the text colour, size & font per note. Change the default text style.


 • Full support for printing notes with AirPrint (iOS 4.2+).


 • Full iOS 4.0 multitasking. Search index updates and Dropbox syncing tasks will complete in the background if the app is closed.


 • Share notes via email without leaving the app.


 • Universal app for both iPad and iPhone.


 • Free and ad-free!



The last "feature" is important. Locayta Notes is and will remain free, as it helps Locayta promote the Locayta Search Mobile full text search library on iOS devices.


Get Locayta Search Mobile here. It is free to download and develop with.


btw, Locayta Notes 1.0 was open sourced on github, as a resource for Locayta Search Mobile developers.



Thursday, February 3, 2011

Janken Battle

Janken Battle: Paper beats Rock!
I recently released a novelty iPhone game to the App Store: Janken Battle.  It is a networked Rock Paper Scissors simulator. You connect to an opponent, either nearby with Bluetooth, or anywhere in the world by Game Center matchup. To play, simply shake the device 3 times, like the kids do in real life. The game handles the rest!  On the 3rd shake, a result is chosen for you and matched against your opponent's result to determine the winner.  The game keeps track of scoring until the end of 3 to 9 rounds.


The result (rock, paper or scissors) is chosen randomly, with equal waiting for the 3 choices by default as you would expect.  However, an option for "advanced" users (if you could call them that) allows you to change the percentages, skewing the result towards your preferred choice.  Or, select a result outright by swinging the percentage to 100% for that choice, leaving the others at 0%.


Janken Battle: Strategy control
This release of the game is really a prototype of an idea I had and a good excuse to play around with and learn the iOS Game Center & GameKit APIs. One of my goals is to see how many modern game features and technologies I can fit into a Rock Paper Scissors game!  It already has:



• Bluetooth wireless play
• Remote Internet play
• Live in-game voice chat
• Online Leaderboards
• Achievements


The graphics & sound (and let's face it, gameplay) are still fairly basic, just what I could put together to get a release out the door. I have a bunch of interesting ideas in mind which should really enhance the game and novelty factor, assuming I get a chance to implement them.  I will probably push them out incrementally as time permits. 


For now, Janken Battle is free, so no excuses not to grab it for a quick play. I have embedded iAds while I consider options for how best to monetize enhanced versions down the line.

Wednesday, January 19, 2011

git push dropbox


Git is great and Dropbox is awesome and recently I found a way to combine the two.


I host my open source projects on github (as well as bitbucket – I'm a happy Mercurial user as well) but there are some private and/or client projects I don't want to store on those sites.


Git, of course, doesn't require a server repository.  So you can work locally while still having full version control.  However, I like the comfort of syncing to an offsite repository so I tend to push my changes to the remote server multiple times per day (also, old svn habits die hard).


For git repositories that I don't want hosted on github, I push to a git repository sitting in Dropbox instead. The push is handled locally and Dropbox performs all the magic of syncing to the cloud (my offsite backup) and to my other computers/devices (switching dev computers isn't a problem). Simple, fast, nice.


Here's how I set up a bare git repository in Dropbox for a project:


$ mkdir ~/Dropbox/src/git-server
$ git init ~/Dropbox/src/git-server/MyProject
$ cd ~/Dropbox/src/git-server/MyProject
$ git config --bool core.bare true

$ cd ~/src/project-git
$ git status
# On branch master
nothing to commit (working directory clean)
$ git push --mirror ~/Dropbox/src/git-server/MyProject

$ git remote add dropbox ~/Dropbox/src/git-server/MyProject
$ git push --mirror dropbox
Everything up-to-date



The first batch of commands initialises a new git repository.  The repository is flagged as bare so that we can push all branches (including the working branch).

The second batch simply pushes a development repository to the Dropbox one.  I use --mirror to push everything over.

The third batch of commands set up a shortcut for the Dropbox repository.  I name it "dropbox" so from then on I only have to do "git push dropbox" to push to it (with or without --mirror as required, although I usually use it).

Update: I have since refined my git/dropbox workflow: git sharing with dropbox.