Just over a year ago, I posted a comparison between two popular online accounting tools, Xero and FreeAgent. As I concluded, I found that FreeAgent worked best for small businesses and freelancers like myself and I’ve been using it ever since.
A portion of my company income comes from the sales of my iPhone app, Squeemote (and I have other apps in the pipeline). Each month, I receive a finance report from Apple that I can download from iTunes Connect detailing exactly what my sales are for that period. If the sales in a particular region have reached more than $150 (or the equivalent), the outstanding balance will be remitted to my business bank account and I will receive a remittance receipt from Apple.
Tracking app sales, the manual way
Ever since I started selling on the App Store, I’ve been using a simple Google spreadsheet to track my sales in each region and I still do. I don’t use this for accounting purposes, but to track my sales in each region, how much I’m due from Apple in each region and the amount received in GBP. This helps me keep an eye on when I expect to receive money for a particular region and how much money I’ve made from Squeemote.
Besides this, I also need a way of getting the figures into FreeAgent to keep my accounts up to date. Previously, I did this manually and the steps I’d follow each month were roughly as follows:
- Log in to iTunes Connect and download the finance reports for the previous month.
- Enter the figures into my Google Spreadsheet.
- Wait for the remittance advice from Apple to confirm that I have received payment for a particular region.
- For each region I’ve received payment for that month, manually create an invoice in FreeAgent detailing the number of units and the unit price from me to Apple (this will be 70% of my app’s sale price net of any sales tax)1 in the appropriate currency. If the payment covers more than one period (because the previous month amounts were under the $150 threshold), I use one invoice line per month.
- Confirm the total matches the finance report.
- When my bank statement has been imported into FreeAgent, explain the transaction from Apple as full remittance of the appropriate invoice; FreeAgent accounts for and reports any exchange rate losses or gains.
It only took several months of doing this before I started to get fed up and realised I must be able to automate some, if not all of the process. Fortunately, FreeAgent has an API so that solved part of the problem; iTunes Connect has no such API unfortunately.
Automation step one: fetching the reports
I decided to split the problem into two parts, the first being fetching the reports from iTunes Connect. Using the Mechanize library, I was able to write a simple Ruby script that logged into iTunes Connect and pulled down the last 10 finance reports, discarding any that had already had been downloaded, and sorted them into a folder for that particular period.
I run this once a month once I have been notified by Apple that the reports are available. I end up with something like this in my Documents folder:

At this stage I still enter the total sales from each report into my Google Spreadsheet manually, including any payments received. It only takes a minute or two.
This script can be used whether you use FreeAgent or not. You will need to install the ‘mechanize’ Ruby gem to use it.
Download fetch_finance_reports.rb from Github
Step two: getting the data into FreeAgent
Using another Ruby script, I can process one or more finance reports into a single draft invoice. I could have the script mark the invoices as sent but I prefer to manually double-check them and mark them as sent manually. The script used to deal with currency conversion automatically but it no longer does this as FreeAgent now has multi-currency support.
Using the script is fairly straightforward. Lets say I’ve just downloaded my finance reports for June and I know I’ve made enough in the EU region and have received a payment for Apple. In this case, I simply run my script against the June EU finance report:
$ cd ~/Documents/FinanceReports
$ process_finance_report 2010-06-June/80062465_0610_EU.txt
This will create a single invoice with a single line for that report, calculated using a unit price and the number of units sold.
Alternatively, let’s say I hadn’t received a payment for the Worldwide region for the last 2 months but as of the latest report, I’ve made enough sales and receive a single payment for May and June. Again, I would run my script but against both reports:
$ process_finance_report \
2010-06-June/80062465_0610_WW.txt \
2010-05-May/80062465_0510_WW.txt
This will again create a single invoice but with a line for each month.
The final part of the equation is making sure the invoices are addressed to the right company. For each region, the payment is received from a different Apple entity; in the US it is Apple Inc, in the EU it is Apple SARL, in Canada it is Apple Canada and so on. My script handles this too although it requires a bit of information up front; after creating different contacts in FreeAgent for each entity, you need to update the script with the correct FreeAgent contact ID. This only needs to be done once.
Download process_finance_report.rb from Github
The required Ruby gems are listed at the top of the script.
If the script seems quite long, its because I’ve taken the various components (such as a simple Ruby wrapper around the FreeAgent API) that I have stored separately and combined them into a single script. There are some variables at the top of the script that need to be customised before you can use it.
You’re welcome to use and modify this script however you see fit. If you modify it, it would be cool if you forked the Gist and made your changes there so anybody can see them but you certainly don’t have to. If you have any issues with the script, you can send me a message on Twitter.
If you’re wondering why its been a bit quiet around here of late, its because I’ve been hard at work on my first iPad application, a client for 37 Signals Campfire. I’ll be talking about that more in another blog post but today I want to focus on something else: OAuth2.
I was invited by 37 Signals’ Jeremy Kemper to try out their in-progress Launchpad + OAuth2 integration platform. I was happy to: the resulting integration would mean a more secure and streamlined experience for my app’s users. Instead of having to add all of their individual Campfire accounts one at a time, they could simply sign in with their 37 Signals identity once and have all of the accounts associated with their ID imported into the app in one go.
Further more, the use of OAuth2 means that my app never needs to store a copy of the user’s username and password. Once they have logged into their 37 Signals identity using a web view, all my app needs to do is store an access token and refresh it as needed. Users can revoke permission for an app to access their account at any time.
Introducing LROAuth2Client
I was quite keen from the outset to open source my OAuth2 implementation and that is what I have done. Documentation is sparse and having been asked on Twitter recently how to use it, I thought I’d write this post to give a basic outline of how it works.
Before continuing, it would be helpful to familiarise yourself with OAuth2 if you haven’t already. The basic gist is:
- A client requests access to a provider’s service using its own unique client ID and secret token.
- The user logs into the service directly (using a web page on the provider’s server) and grants the client permission to access.
- The provider redirects the user to a URL unique to the client passing along a verification code in the query string.
- The client verifies the authorization request and uses the verification code from step 3 to obtain an access token.
- The client may periodically refresh the access token when it expires.
The above sequence describes the “web server” flow as outlined in the draft OAuth2 spec; there are other flows but this is the only one supported by LROAuth2Client right now and is the step I will outline below.
Getting started
To get started with LROAuth2Client, you will need the following:
- Your client’s unique ID, redirect URI and secret token. These are typically provided after registering your client with the provider.
- The end-user endpoint URI. This is where you will redirect users so they can sign in to the provider’s service.
- The token URI. This is used in conjunction with the verification code to obtain an access token.
If possible, you should try and obtain the end-user and token URIs at runtime rather than hardcoding them into your application. This can be typically done by making an unauthorised request to a secure URI and obtaining them from the WWW-Authenticate header in the returned 401 response. 1
Creating an instance of the client
Having established that you need to ask the user to authorise your app for access, you need to create an instance of LROAuth2Client.
// oauthClient is an instance variable
oauthClient = [[LROAuth2Client alloc]
initWithClientID:@"MY_CLIENT_ID"
secret:@"sssh_top_secret"
redirectURL:[NSURL URLWithString:@"myapp://oauth"]];
oauthClient.delegate = self;
You’ll notice that the redirect URL can be any valid URL - I recommend using a custom scheme for your app. LROAuth2Client will intercept calls to this URL for you. If the provider only supports HTTP or HTTPS URLs, it will intercept those as well.
At this point, you will need to configure the client with the end-user URI and the token URI. As I mentioned earlier, the best way of doing this is to send an unauthorised request and obtain these from the returned header but for the purposes of this tutorial, we will hardcode them:
oauthClient.userURL = [NSURL
URLWithString:@"http://myawesomeservice.com/authorization/new"]
oauthClient.tokenURL = [NSURL
URLWithString:@"http://myawesomeservice.com/authorization/verify"]
Authenticating the user
To actually ask the user to authenticate, you will need to construct a UIWebView, which will load the request for the end-user URI. In these examples, we’ll assume we have an instance of UIWebView already loaded from a NIB file and stored in an outlet called myWebView. You will then need to ask the client to authenticate the user using that webview:
[oauthClient authorizeUsingWebView:myWebView];
LROAuth2Client will assign itself as the web view’s delegate and make the request to the end-user URI. Once the user authenticates and authorises access the provider will redirect to your client’s redirect URL.
LROAuth2Client will intercept a request to this URL, cancel the request and extract the verification code from the request query string. It will then make a POST request to the token URL using the verification code and assuming everything works as it should, will receive an access token that can now be used to authorise any requests your app makes.
When the whole process has finished, LROAuth2Client will keep a pointer to the access token (an instance of LROAuth2AccessToken) and notify its delegate. You can use this delegate method to store this access token somewhere that can be accessed by the rest of your app. Because LROAuth2AccessToken implements the NSCoding protocol, you could simply save the token to disk, as follows:
- (void)oauthClientDidReceiveAccessToken:(LROAuth2Client *)client;
{
LROAuth2AccessToken *token = client.accessToken;
[NSKeyedArchiver archiveRootObject:token toFile:@"Path/To/MyAccessToken"];
}
Refreshing the token
Access tokens will typically expire after a certain amount of time. LROAuth2AccessToken has an expiresAt property (an instance of NSDate) and a refreshToken property. If the token has expired, you should use the client to refresh that token and save the updated token to disk. An example implementation might look something like this:
- (void)checkAccessTokenForExpiry:(LROAuth2AccessToken *)accessToken;
{
if ([accessToken hasExpired]) {
[oauthClient refreshAccessToken:accessToken];
}
}
- (void)oauthClientDidRefreshAccessToken:(LROAuth2Client *)client;
{
LROAuth2AccessToken *token = client.accessToken;
[NSKeyedArchiver archiveRootObject:token toFile:@"Path/To/MyAccessToken"];
}
The bigger picture
In case you were wondering how you would put this all together in your application, I’ve created a sample XCode project that you can download from Github. It uses the new Facebook graph API to retrieve a list of your Facebook friends and uses OAuth2 for authorization. To get started, just follow the instructions in the provided README.
At present, LROAuth2Client only does what I needed it to do. It has the following limitations:
- Only supports the web-server flow.
- Only supports services that return the access token in JSON or form encoded string format.
- Has a dependency on ASIHTTPRequest and TouchJSON.
I’m very open to pull requests so if you feel like stripping away the ASIHTTPRequest dependency and using NSURLConnection instead, please, go ahead. If you want to add XML support and support for the other flows, that would be great too. If you have any feedback, send a tweet to @lukeredpath.
Since moving from a server-side blogging system to a statically generated site I’ve struggled with blog comments. I’ve tried Disqus and I’ve tried Intense Debate; I preferred the latter by far but neither really felt right. They were clunky, they slowed down page response times and integrating them into the site’s design was more hassle than it was worth.
A couple of months ago, I decided to just remove comments from my blog entirely. It was something I had thought about for a while, ever since I read my FreeRange colleague James Adam own thoughts on blog comments.
I’m still torn on the subject; I like not having to manage a commenting system and I encourage the idea that your own blog is a better platform for your thoughts and feedback on what somebody else has written.
That said, I still like to get feedback on my posts, particularly posts about my open source code where the feedback can be particularly useful in driving a project forward and I’m still not sure what the best solution for that is outside of blog comments.
For the meantime, I will be keeping comments off of the site. If you want to send me a message about one of my posts, feel free to send me a message on Twitter. If you want to leave detailed feedback, criticism, praise or discussion of one of my posts, write your own blog post and send me the link so I can read it.
There are a number of techniques for loading instances of UITableViewCell from a nib file to use in your table views; up until recently, I thought that the best way was the Apple recommended way (developer documentation link). It also comes recommended by Jeff LaMarche, who discovered the technique after writing his book, Beginning iPhone Development.
The technique itself isn’t a terrible but as the comments on Jeff’s article will attest, there is a perception that the technique is hacky, or non-intention revealing. I’m not sure if I’d go as far as calling it a hack, but it certainly lacks enough clarity that somebody not familiar with the technique could find it potentially confusing.
The biggest issue I have with the technique is that it in order to wire up your custom cell with an outlet, your cell nib’s file owner needs to be set to the controller that is using the cell. This introduces an unnecessary dependency on the controller and makes it harder to use in other controllers.
An alternative solution, as presented in Jeff’s book is to loop through the array of bundle objects returned by loadNibNamed:owner:options to find your cell.
...
if (cell == nil) {
NSArray *items = [[NSBundle mainBundle] loadNibNamed:@"MyCustomCell" owner:self options:nil];
for(id item in items) {
if ([cell isKindOfClass:[UITableViewCell class]]) {
cell = item;
break;
}
}
}
It’s a few more lines of code but it lends itself well to extraction into a custom category on UIViewController.
- (UITableViewCell *)loadTableViewCellFromNibNamed:(NSString *)nibName;
{
UITableViewCell *cell = nil;
... // as above
NSAssert1(cell, @"Expected nib named %@ to contain a UITableViewCell", nibName);
return cell;
}
In addition, I’ve added an assertion to ensure the nib file contains a cell and to fail fast if it doesn’t (I can’t think of any good reason why you would want this method to ever return nil).
One problem that both techniques have is that they require the reuse identifier to be set in interface builder; if you forget to do this, your cells will not be re-used and your nib file will have to be loaded for every cell in your table view.
In a recent article, Jeff LaMarche once again offers some advice on how to avoid this problem. It’s a reasonable solution but requires a little more boilerplate code than I like - it will also not work if you need to use multiple reuse identifiers, something that Jeff points out in his article.
Instead, I offer an alternative, less intrusive, defensive approach to the problem. In the same way that we can use a simple assertion to ensure our nib file contains a cell, we can use an assertion to check that the cell also has a reuse identifier.
Originally, I added the assertion to the same method but realized that there may be times where you legitimately do not want to set a reuse identifier (e.g. for single-use cells), so I simply introduce a second method in the category:
- (UITableViewCell *)loadReusableTableViewCellFromNibNamed:(NSString *)nibName;
{
UITableViewCell *cell = [self loadTableViewCellFromNibNamed:nibName];
NSAssert1(cell.reuseIdentifier, @"Cell in nib named %@ does not have a reuse identifier set", nibName);
return cell;
}
Now, you still need to set the reuse identifier in Interface Builder, but by using this method we will always fail fast if we forget to do so.
There is also no reason why this technique cannot be combined with Jeff’s; if you are using a sub-class of UITableViewCell with your nib-based cell you could easily override the reuseIdentifier method to return a static string but it’s up to you.
So after all of this, the table view’s tableView:cellForRowAtIndexPath: method ends up looking like this:
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyCustomCell"];
if (cell == nil) {
cell = [self loadReusableTableViewCellFromNibNamed:@"MyCustomCell"];
}
...
There is still one issue left with this solution: the reliance on a string identifier is still prone to error. A single typo in the identifier in Interface Builder would stop cell reuse from working but would not be picked up by the compiler or the assertions.
This is certainly one advantage of Jeff’s solution, which avoids setting the reuse identifier in the nib file completely, but we can still defend ourselves against this problem by introducing a simple and obvious convention: always use the same string for your cell identifier and your cell’s nib name. By adhering to this convention, we can add one final check to the category method:
- (UITableViewCell *)loadReusableTableViewCellFromNibNamed:(NSString *)nibName;
{
...
NSAssert2([cell.reuseIdentifier isEqualToString:nibName], @"Expected cell to have a reuse identifier of %@, but it was %@", nibName, cell.reuseIdentifier);
return cell;
}
Conclusion
Both approaches have their pros and cons. For controller-specific single-use cells, bundling the cells with the nib file for that controller and hooking them up to outlets is probably the simplest solution and is the method I would choose. For general loading of reusable cells, this method seems clearer to me.
The UIViewController category can be downloaded from GitHub.
Authentication support was recently added to Pusher so I took the opportunity to add support to libPusher for triggering events using the Pusher REST API.
As part of my implementation, I’ve introduced a new, higher-level API in the form of PTPusherChannel. It sacrifices some flexibility (by using a shared API key, secret and app ID) for the sake of ease of use and attempts to mirror the way the Ruby Pusher client gem works. All of the lower-level API detailed in my original blog post is still available.
To use it, you first need to configure PTPusher with your credentials; a good place to do this is in your applicationDidFinishLaunching app delegate method:
- (void)applicationDidFinishLaunching:(UIApplication *)application
{
[PTPusher setKey:@"your api key"];
[PTPusher setSecret:@"secret"];
[PTPusher setAppID:@"app id"];
...
}
Once you have done this, creating new channels is easy:
myChannel = [[PTPusher channel:@"demo"] retain];
Note that the channel methods returns an autoreleased PTPusherChannel instance so you will need to retain it and release it when you are finished with it.
To trigger a new event, simply call the triggerEvent:data: method; the data parameter can be a string or any Objective-C object that can be serialized as JSON, such as an array or dictionary:
NSArray *objects = [NSArray arrayWithObjects:@"one", @"two", @"three", nil];
[myChannel triggerEvent:@"demo-event" data:objects];
In addition to being able to trigger events, PTPusherChannel offers an alternative means of receiving events on that channel using a delegate:
- (void)viewDidLoad;
{
...
myChannel.delegate = self;
}
- (void)channel:(PTPusherChannel *)channel didReceiveEvent:(PTPusherEvent *)event;
{
// handle event
}
Here is a small screencast of the included sample app triggering and handling its own events.
For more details, check out the updated README on GitHub and take a look at the sample app.
Earlier today, New Bamboo announced their new Pusher service. Pusher is a centralized service that allows you to distribute real-time events from your web apps to the browser using HTML5 WebSockets.
It immediately struck me: why limit yourself to the browser? What if your iPhone (or iPad) app could receive those events too? Sure, you could use the Apple Push Notification Service, but why go through all the hassle that entails just to send events from your server?
If you could use the same event-distribution mechanism for your real-time HTML5 browser interface as your iPhone/iPad (or any other) interface, that’s an instant win.
Getting started with the Pusher Objective-C client
So, with just the existing Javascript reference client to go on, I set about trying to create an Objective-C client. I’ve tried to mirror the Javascript API where it makes sense, but using a more Cocoa-centric approach (e.g. target/selector binding and notifications).
Using the library is fairly straightforward and there are principally two ways of using it, which I’ll explain here. If you haven’t already, you might want to read up on how Pusher works.
First of all, you need to create an instance of PTPusher for the channel you want to monitor.
- (void)applicationDidFinishLaunching:(UIApplication *)application {
// pusher is an instance variable in my app delegate
pusher = [[PTPusher alloc] initWithKey:@"YOUR_API_KEY"
channel:@"THE_CHANNEL_NAME"];
}
Once you have a PTPusher instance, you can start registering for events. Each event listener requires a target and a selector.
[pusher addEventListener:@"my-event"
target:self selector:@selector(handleEvent:)];
The event callback method will receive a single argument, the PTPusherEvent object representing the event. Here’s what handleEvent: might look like:
- (void)handleEvent:(PTPusherEvent *)event;
{
// lets just log the event name and data
NSLog(@"Received event %@, data: %@", event.name, event.data);
}
As you can see, PTPusherEvent has two properties: name and data. The data property will return the deserialized JSON data as a native Objective-C object, typically an NSDictionary.
The second approach is to use notifications. Whenever PTPusher receives an event, it will post a PTPusherEventReceivedNotification. This will allow you to respond to events across your application without necessarily knowing about a specific PTPusher instance:
// register for event received events
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handlePusherEvent:)
name:PTPusherEventReceivedNotification
object:nil];
In your notification handler, you will be able to get the PTPusherEvent from the NSNotification’s object property.
Because your notification handler might not know anything about the PTPusher that sent it, you might want to check the channel from which the event arrived before deciding how to handle it. You can do this by using the channel property of PTPusherEvent.
- (void)handlePusherEvent:(NSNotification *)note;
{
PTPusherEvent *event = note.object;
if([event.channel isEqualToString:@"some_channel"]) {
NSLog(@"Received event %@, data: %@", event.name, event.data);
}
}
Obviously, both Pusher and the Objective-C client are still in their early stages. There is still some work to be done with the client, including proper error handling, but there’s a lot of potential to create some interesting, cool applications using this. I already have some plans to integrate it with the work I’ve been doing on synching web services with Core Data.
I’ve recorded a small screencast that shows the library in action. Grab yourself a Pusher account and API key and have a play. As always, the code is available on GitHub.
When creating iPhone and iPad apps that communicate with web services (typically some kind of REST API), it is often desirable to cache data wherever you can - your app shouldn’t be dependent on a network connection when it doesn’t have to be.
To cache or not to cache
Caching is a notoriously difficult issue (or rather, cache expiration is) and there are many ways of going about it. One way of doing it would be to create a mirror of the remote objects in a local Core Data store. This would allow you to take advantage of everything that Core Data has to offer and lets you more easily create apps that work offline, as they have their own object model to work with and are not dependent on the remote API.
The biggest issue with this approach is keeping the local and remote objects in sync with each other. You would need to be able to:
- Populate your Core Data store initially with data from the API
- Keep local objects updated with remote changes
- Keep the server updated with local changes either immediately (if a network connection is available), or in the future (when a network connection becomes available).
The ideal solution would be relatively seamless. Your application would deal with its own object model and the synching would just work.
On possible solution: Core Resource
Whilst there are number of solutions to synchronzing Core Data stores across devices (such as Apple’s own Sync Services framework, and Zsync), the only attempt to solve the issue of synching with a web service is Core Resource.
Core Resource seems like a cool project, but based on my initial impressions, it makes some design decisions that I felt were inappropriate:
- Core Resource knows too much about your remote resource. My preferred solution would be completely agnostic to how your remote resource is fetched. My preferred solution doesn’t prescribe a particular type of web service (REST, SOAP etc.) nor how that data should be fetched.
- Related to the above, because Core Resource concerns itself with fetching the remote data, it has a dependency on ASIHTTPRequest. ASIHTTPRequest is a pretty cool library, but what if I want to use the awesome HTTPRiot instead?
- Core Resource depends on inheritance. All of your entities need to inherit from
CoreResource, which in turn inherits from NSManagedObject. My preferred solution makes no assumptions about your class hierarchy and doesn’t force unnecessary coupling; I feel that protocols would be the better, more Cocoa-centric approach.
- Core Resource has some cool user-interface related widgets (such as a custom UITableViewController) but I’d prefer if my synching library focussed on just one thing - synching - whilst allowing your app to hook into certain events using notifications and delegation.
Working towards a solution, current progress
Over the past week, I’ve been working on my own solution to this problem. It’s in the very early stages and has changed a lot already. I’ve made decisions which turned out to naïve or just plain stupid. But that’s OK.
So far, I’ve got basic synching with a single entity in place. Fetching, creating, updating and deleting works as you’d expect. Association support is non-existent. The code is up on GitHub - I encourage you to look not only at the code, but the commit log too, as it is a reasonably good account of the various decisions I’ve made and then changed my mind about as I’ve worked on this.
The code is all wrapped up in an example app that uses HTTPRiot to interact with a bundled Rails 3 app which provides a REST interface (using standard Rails scaffolding) to its object model (currently just one entity).
All code that relates to synchronisation is agnostic of how and where the remote data comes from and that should be clear from the design. I’ve chosen to use a Rails app and HTTPRiot for convenience only. Over time, I will start to separate out the synching components from the main application.
Moving forward, a request for feedback
With the advent of the iPad, I think apps that interact with remote services are going to take an even more prominent role. I have a few ideas of my own that I hope would be able to use the code that I am writing now. I’d love it if others were able to eventually start using my code to write their own Core Data backed, web resource synched apps too.
I’d love to get some community involvement with this project. One of the reason I pushed it to GitHub so soon was to get it out there and make it real. I have so many projects in the dark corners of my file system that never see the light of day and I didn’t want this to be one of them.
I wanted the decisions I make in the development of this to be as public as possible. I encourage other developers - especially those with good Core Data experience (which I lack) - to get involved. Use the GitHub Issues page to ask questions and leave feedback. Fork the project and see where you can take it.
Comments on this blog are still disabled, so if you want to make your thoughts known, as well as using the aforementioned GitHub issues page, I encourage you write your own blog post and @message me the link on Twitter and I’ll happily update this post with links to interesting feedback.
Now what are you waiting for? Get forking!
On a recent FreeRange iPhone hack-day, we spent some time adding basic search functionality to a UITableViewController using some sample code provided by Apple. The sample code uses the UILocalizedIndexedCollation class to split an array of items into an array of arrays – one array per section – and display those accordingly.
This approach works fine, until you decide you want to add a search bar and the little magnifying glass symbol into your scrollable index – just like Apple’s own apps – to go with it.
Whilst this is easy enough (all you need to do is return UITableViewIndexSearch as one of the elements in your section index titles array), this insertion of a new element into the section index means the section index passed into UILocalizedIndexedCollation needs to be offset by one to take this into account, for example:
- (NSInteger)sectionForSectionIndexTitleAtIndex:(NSInteger)indexTitleIndex
{
if(indexTitleIndex == 0) {
return NSNotFound;
}
return [collation sectionForSectionIndexTitleAtIndex:indexTitleIndex-1];
}
Whilst this is a relatively trivial fix, it would quickly become a pain if you have to do this everywhere you use a UITableView with a section index and a search bar. For that reason, I wrapped up UILocalizedIndexedCollation in a very simple decorator class which injects the magnifying glass icon into the section titles array and handles the offset for you.
The full code is available on a GitHub gist. It is designed as a direct drop-in replacement for UILocalizedIndexedCollation and as such implements the same interface. Simply swap out the calls to UILocalizedIndexedCollation in your existing delegate methods with calls to FRIndexedCollationWithSearch. The gist includes some sample usage.
One final tip: to have your UITableView scroll to the top (and reveal its header containing the search bar) when your user taps the magnifying glass icon in the search index, you can catch the NSNotFound returned by the collation in the tableView:sectionForSectionIndexTitle:atIndex delegate method:
- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index
{
NSInteger section = [[FRIndexedCollationWithSearch currentCollation] sectionForSectionIndexTitleAtIndex:index];
if(section == NSNotFound) {
[tableView setContentOffset:CGPointMake(0,0)];
}
return section;
}
View a list of all previous posts