Sunday, August 01, 2010

Customising the Appearance of UITabBarController

(I tried out a couple of titles for this post, one of which was "Customising the Appearance of UITabBar". But I realised that, while in effect this is how you could describe the problem, it wasn't really accurate. To all intents and purposes, UITabBarController is a single opaque entity. Yes, there's a UITabBar in there somewhere, but that knowledge alone is of little use to you. I get the feeling that UITabBar has its own documentation only because otherwise UITabBarController's would be even longer than it already is.)

The Problem: Whichever finger painter you're currently beholden to has decided that your next app is going to use a tab bar. Only not Apple's tab bar. Oh no. Because that's what ever other app looks like and it just doesn't look designed and anyway they want all seventeen tab buttons to appear at once without that "More" button thing and mwaa mwaa mwaa mwaa mwaa.

So you're left with the task of reimplementing UITabBarController, something which originally took a team of Apple developers about three times as long as you've been allocated for the entire project. And you know that you won't even get close, because you'll loose all that useful Apple goodness like -viewWill(Did)(Dis)Appear messages which actually propagate to sub-view controllers and being able to honour hidesBottomBarWhenPushed from the depths of the navigation stack.

What you'd really like to do is just change how the regular UITabBar displayed by UITabBarController looks.

The Warning: I'm using this code in a production project, but I haven't submitted it to the App Store yet. There's a chance that adopting this approach may get your app rejected. (Although chances are that's more likely to happen bceause your new style tab bar looks too much like Apple's own, rather than because of anything happening at the code level.)

This has been tested on 3.1.3 and 4.0.1, but it's exactly the kind of evil hackery which is likely to break with a future iOS upgrade.

The Solution: We're going to create a UITabBarController subclass (yes, even though Apple says not to in the second line of the docs). Here's the code. And here is an example project (based on the default iPhone tab bar template) for the impatient among you).

SJCTabBarController.h:

#import <uikit/uikit.h>

@interface SJCTabBarController : UITabBarController {
UIView *_fakeTabBar;
UIButton *_currentSelection;
}

@property (nonatomic,assign) IBOutlet UIView *fakeTabBar;

-(IBAction)fakeTabTapped:(id)sender;

@end

SJCTabBarController.m:

#import "SJCTabBarController.h"

@implementation SJCTabBarController

@synthesize fakeTabBar=_fakeTabBar;

// evil trickery happens here
-(UITabBar *)tabBar { return nil; }

-(void)viewDidLoad {
[super viewDidLoad];

// install our fake tab bar
[[super tabBar] addSubview: _fakeTabBar];

// set up the default selected tab
// you may like to read the tab/tag number from the user defaults
[self fakeTabTapped: [_fakeTabBar viewWithTag: 0]];
}

// switch tabs
-(IBAction)fakeTabTapped:(id)sender {
// do we need to do anything?
if(sender == _currentSelection) return;

// un-select the currently selected button
_currentSelection.selected = NO;

// select the new button
_currentSelection = (UIButton *)sender;
_currentSelection.selected = YES;
self.selectedIndex = _currentSelection.tag;

// you may like to write the selected index into user defaults here
}

-(void)viewDidUnload {
[super viewDidUnload];

_fakeTabBar = nil;
_currentSelection = nil;
}

@end

Explanations in a second, but first the Interface Builder part of the equation.

In IB, drag in a UITabBarController (or use the one from the MainWindow.xib created with the default tab bar XCode template) and add view controllers to it. Then change it's class to our UITabBarContoller sub-class. Now add a view to act as your new-look tab bar. This should be a screen wide by the standard 49 points high. Connect it to the fakeTabBar outlet in the subclass. Give it a non-zero tag higher than the number of tabs you want to add.

Now add a button for each tab. Give them a tag which is the same as the index of the view controller you want selected when they're tapped (eg. the first is 0, the second 1, etc.). Wire them up to send -fakeTabTapped: to the sub-class.

You should end up with a hierarchy like this (I've added a total of six view controllers to the tab bar controller in order to demonstrate how awesomely it all works):



Which will produce something like this (which, by the way, is why no designer working with me needs worry about their job security):



So what's going on in this code? It's really rather simple, and yet I couldn't find anything quite like it on the Googles (which suggests that I've done something incredibly stupid and just not noticed it. I bet you have. The comments are down there). Our custom tab bar is added as a subview of the existing tab bar. We access this via [super tabBar] because we've overridden the getter for the tabBar property to return nil. We do this to stop Apple's code (which is well behaved and seems to always use the property accessors rather than going straight to the ivar) from altering what it thinks is a standard tab bar. Comment-out this method and you'll see a ghostly "More" tab and various labels appearing.

One last fix. You'll notice that when you select a tab which should have been managed by the "More" tab, a navigation bar with a back button entitles "More" will automatically appear at the top of the view. You can remove this by calling

[self.navigationController setNavigationBarHidden: YES];

in the -viewWillAppear: method of these view controllers. This has the unfortunate side effect of preventing you from using a navigation controller-managed navigation bar in these controllers, but the chances are that if your designer doesn't want to use the standard tab bar, they won't want this behaviour either.

(Edit: And immediately after posting this I realised that, in -fakeTabTapped:, instead of exiting if the same tab was selected again we should be checking for a navigation controller and popping all of its subviews. I'll leave that as an exercise for the reader.)

Saturday, November 21, 2009

Shine: Your Mac Indie Business in a .zip

Last week I released SimCap, my first OS X application. This entailed — in addition to the whole 'designing and coding and testing' part — setting up the infrastructure necessary to sell it. Of course, I did all this at the very last minute, with SimCap sat there ready to go and the absolute minimum of advanced planning. That minimum planning was the decision to use Shine.


Shine is an application management dashboard, written by Tyler Hall and designed around the basic workflow of your average Mac Indie developer: taking payments via PayPal, generating licences with AquaticPrime, and pushing updates using Sparkle. I first heard about Shine on The MDN Show (epsiode 11) (which was — sorry, Scotty — the first episode I'd listened to in a long time). One of the things about iPhone development is that Apple does take care of most of the business side of things for you. While Shine doesn't exactly sit you down with a hot cuppa and tell you not to worry your pretty little head about all those nasty numbers, it certainly makes things easier, especially if you're starting out from scratch. Assemble components to fulfil the same roles Shine does would probably taken a few days.


I'm afraid that this won't be a complete Shine "how to" guide, but instead a collection of my thoughts on the process of setting the dashboard up, scribbled down while the experience is still fresh in my mind.


Firstly, did you notice that "especially if you're starting out from scratch" bit above? I mention it because Shine is rather inflexible in what it does. You can replace the AquaticPrime portion with your on licence generator, but otherwise the workflow is pretty-much locked down. Payment info comes in via PayPal's IPN service, so if you use a different payment processor you'll either need to write your own handler or look for another solution. Likewise, updates are stored in and downloaded from Amazon S3, without, say, the option to store them on your local server. In this last case, I decided that the ease of using Shine would probably outweigh the cost of using S3. (And I think I was right: this week's thousand downloads have cost me about 20 cents.)


Shine is basically a set of PHP scripts (available from the link above, either directly via Git or in the archive format of your choosing). Installation is just a matter of copying them to your server. The config file allows you to configure the exact same code for both your test and production servers. Documentation is minimal, basically amounting to the comments in the source code — but then this is aimed at Indie developers, who can probably be expected to feel comfortable diving into the code and figuring things out that way.


But just in case you aren't, or you've stumbled across this post looking for help... You build the MySQL database shine needs by running the .sql file from the archive using MySQL'a source command. You'll then have to create your own entry in the users table. There's an entry for using encrypted passwords in the config file, and if you set it, don't forget to also provide a string of random characters as salt. I ended up inserting random echo statements into the hashing function (on my test server) to see what value I should enter into the database as a password, but I'm almost certain there's a simpler way of doing it.


I can't personally say that installing Shine on my production server was a breeze, but none of that was Shine's fault. The code is neatly modular, which means that you may only encounter a problem with your server configuration the first time you go to use a particular feature. For instance, the first time I tried to test the PayPal IPN, I discovered that Shine needed libcurl, which I didn't have installed. (What followed was a couple of days of server re-imaging and mod_php re-compiling — but I'm sure that your mileage will vary. Especially if you have a grown-up looking after your servers for you.) As far as I can tell, the not-really-at-all-exotic-although-some-Linux-distros-seem-to-think-so modules you'll need are libcurl (for PayPal IPN), libjson (for everything(?)), and bcmath (for AquaticPrime — and this can only be added to PHP at compile time... sigh). A little pre-flight test script to check for these and warn of any potential nasty surprises might be an idea (and I guess if someone who knows their way around PHP doesn't write it then I'll have a go...).


Once you get this far, another little gotcha is that Shine will only process IPN messages with "Completed" status. Which makes perfect sense and is how you want it to behave, but can easily catch you out. If, like me, you're selling in a different currency to that of your bank account, you'll want to throw the "accept and convert" switch (it's under "Profile > Payment Receiving Preferences") to have transactions automatically completed. And while you're there, you'll also probably want to disable accepting eCheques, since transactions using them also won't complete straight away.


I've dwelt on the cons of using Shine because a post saying "use Shine, it's simply amazing" would be a little dull. But, honestly, I really can't recommend it enough. It has made the business side of launching an OS X application about as painless as you could hope. (And I haven't even mentioned it's integration with Tyler's OpenFeedback framework.) There are a couple of extra features which I'd like to see — some indication of the sample size used to calculate your Sparkle stats (did I mention Shine will also collect statistics from Sparkle?), for instance, and maybe auto-response and notification e-mails for OpenFeedback submissions — but otherwise Shine does everything it needs to so you don't have to. Get it and use it: it honestly is that much of a no-brainer.



Saturday, November 14, 2009

Apple Does Love Me!

I'm sorry if I ever doubted you guys.

Is the App Store Broken?

You could say that these are Interesting Times to be an iPhone developer — although you could easily have said the same thing at any point since the announcement of the SDK. It seems that time and again we get weeks like this, where the number of "App Store Evil" stories in the press suddenly swells and it looks like matters are coming to a head. There have certainly been some happenings recently. The more public events you've doubtlessly heard of from Gruber, so I'll start with what's been going on behind the iPhone Dev Centre log in.


Trouble in Store


If you're a frequent browser of the App Store you may — or more likely, may not — have noticed something a little funny about the listings; in particular the "Recent" lists. Apple has introduced a new updates policy for the App Store. It used to be that whenever an update to an app was released it would be bumped back up to the top of the relevant list. This is no more. Now this list only shows newly released v1.0 applications. To say that this change is unpopular among developers would be an understatement.



For many developers, frequent small updates — and their accompanying bump in position with attendant spike in sales — was their main (or only) business plan. (I can certainly attest that the only times Lexical has seen any number of sales were after initial release and after the 1.1.) Cue much wailing, gnashing of teeth and cries of "sod this, I'm off to Android", interspersed with pragmatic — and somewhat exasperated — messages which basically boil down to: "Oh FFS, you can't rely on Apple to do all your marketing for you, why don't you try a little advertising, I hear it works wonders in every other single business ever."



As a side effect of these changes — or, more likely, of cracking the App Store open and tinkering with its workings — we get the messed-up listings. Apps released last year are suddenly reappearing at the top of lists, accompanied by other apps which won't be released until next year. On the plus side, you no longer have to manually adjust your release date once your app has been approved, but at this moment that seems like a very small comfort. It's been over a week now and things finally seem to be settling down. Presumably it's taken so long because those whose job it is to fix it — and who made the mistake in the first place — have been nailed up in the lobby of their building on Infinite Loop — alongside the rotting corpses of the old MobileMe team — as a warning to others.



(Playing armchair App Store manager for a moment, I'd suggest a two-track approval process for updates. When the developer submitted their update they'd check one of two options: "Bug fix" or "Feature Update". Bug fixes would be fast-tracked — let's say there's a semi-guarentee of under five days — but would not receive a list position bump. Feature Updates would be subject to the same fourteen-days-if-you're-lucky process as new apps — and as we get by default now — but would benefit from top-of-the-list exposure. But since I do not, have not and probably never will run a international application download service it really doesn't matter two hoots what I think should happen.)



What's in a Name?


Meanwhile, TUAW reports that a number of developers have been sticking the word "edge" into their app names as a protest against the actions of the owner of the trademark "Edge", who has been busy enforcing it — with Apple's help — in the App Store. I suppose it's good to see that, despite their world collapsing around them, iPhone developers can find the time to have a little silly fun. The word "edge" isn't the only one to be proscribed in this way. "Memory" is also now verbotten, although it seems on Jeff Lamarche has reported on this. So should we take this as another sign that the App Store is broken? No. In this case, Apple is responding to a legal request in the only way it can. Whether or not that request is fair is not their decision.



I'm Going Home and I'm Taking My Toys with Me


We've had a couple of high profile hissy-fits in recent days. I'll include that Facebook guy under "high profile", even though I'd never heard of him before. But we shouldn't be too surprised by his departure. If he works for Facebook then he's going to be rather limited in the type of apps he's gets to work on (hint: it will mostly be Facebook clients). The Facebook iPhone app has been around long enough that it probably does everything it needs to, so there's no point wasting your rock star developer on maintaining it. Might as well get yourself some free publicity with a spectacular walkout and then hand the day-to-day upkeep to some lacky.


And then we have Mike Ash and Rogue Amoeba. The Aitfoil Speakers Touch saga is being held up as the latest great failing of the review process and of how the rule reviews enforce are arbitrary, obscure and inconsistent. (Although at least no-one seems to be blaming this on "out-sourcing" and "off-shoring" any more. Those terms rank alongside "UK-only call centres" for making me want to stab people in the face.) But just for the sake of being contrary, I'm going to disagree. I think the — admittedly unspoken — rule is very clear: Apple is asking iPhone developers, "Please do not let users think that your application is our problem".



The Airfoil problem boils down to this: Rouge Amoeba want it to show (a) a Mac OS X application icon, and (b) an image of a Macintosh computer; Apple don't. Rogue Amoeba's first argument is that neither icon nor image are distributed with the app, but are instead accessed using public API calls on the host machine. Doubtless true, but still beside the point. If I try to release XXX iHardcore iPorn I shouldn't be surprised if it's rejected, no matter how loudly I complain that the content isn't included in the app but is downloaded using public APIs. A high proportion of the time, the icon which Airfoil displays will be from an Apple product. 100% of the time it will display one of Apple's images of a Mac. When an application does something 100% of the time arguments about how it does it are nothing more than weasel words.



The second argument is that the first version of Airfoil did exactly the same thing and was both accepted and remained available for the whole time. Which is where — and I'm just speculating wildly here, but then so are you, so shut up — the unspoken "Please do not let users think that your application is our problem" rule comes in. Maybe the person whose job it is to trawl through the support logs found an abnormal number of requests which went something like: "I'm having terrible problems with your iPhone application X and if you don't fix it right away I'm telling my lawyer" "But sir or madam, X is not an Apple application" "Oh no you don't fool me that easily, it has your a picture of [random Apple product] in it so it must be yours". Airfoil need not even have been the product mentioned. We already know that you cannot use images of the iPhone in iPhone apps. Images of any Apple product seems like a natural extension of this. And since we've all seen the App Store evolve over time, it's not an enormous leap to assume that this new tightening of the policy was instigated sometime between the release of the original Airfoil and the submission of the update.



The iPhone and iPod Touch are not Macs, and not only from a technical point of view. They are consumer electronic items in a way that computers never really have been. As such, they have a different user base (although admittedly with a lot of crossover). Let's try a little experiment. Find a non-Apple-devotee iPhone user. Take their iPhone and pull up the home screen. Now ask them to identify which of the apps listed there come from Apple and which don't. Product support costs money. Yes, Apple makes great margins, but they don't do it by paying their staff to explain time and again to customers why the problems caused by that app showing a picture of a Mac and the iTunes logo isn't actually anything to do with them. (After all, Apple is more than capable of causing its own bad publicity, thank you very much.)



Let me try a different metaphor. You may be familiar with the "advertising feature" which you occasionally find in magazines and newspapers: an advert formatted to look like editorial. Now, I'm willing to bet that your average journalist hates these more than an over-zealous expenses auditor. Because you may have the smartest readership in the world, but when they're curled up on the sofa with your publication, defences down, flicking away, there's a good chance that a least a few of them may be tricked into thinking that it's actually you or your colleagues who are recommending that they install Super-Dooper Virus Smasher 2012. I'm sure that Apple views apps that appropriate their content in exactly the same light.



And... I think that's more than enough for now.



Friday, November 13, 2009

Plaques + SimCap = A Busy Week

It's like the tired old joke about buses: I wait months (no, years) to release any applications, and then suddenly along come two at once.



First, on — I think — Monday, late, my second iPhone application Plaques, eventually limped into the App Store. Where it was promptly buried on page 4 by a slew (correct collective noun) of near-identical travel guides. Since then its sold a couple of copies but generally not done very well.



And then today we have SimCap, my first proper grown-up OS X app. It's a deceptively simple little application which does some eye-wateringly nasty stuff with window buffers and uses an NSOperationsQueue and everything. (In my spare time I've been trawling StackOverflow for questions about NSOperations being either delayed or cancelled, so I can chime in and suggest the poster checks for typos, because it sounds like they've accidentally sub-classed NHSOperation... this is actually a very funny Cocoa coder joke.) I'm really very happy with the way this has turned out.



So all in all a pretty busy week. I know, it's really amazing the lengths I'll go to to avoid my NaNoWriMo obligations.



Saturday, October 10, 2009

Why the Hell am I Still Developing for the iPhone?

iPhone development — or in particular, the App Store — continues to appear to be an unpleasant place to be. First up there's the Tweetie 2 debacle (covered here by Dan Moren, who's one of my favourite Mac journalists, but who seems to care a little bit too much about currying favour with the Mac dev community). I've got nothing to add to the upgrade pricing fuss — if the developer can get people to pay then good luck to them — but I will just pull out this from Gruber:

I’ve been using it for a few weeks, and there’s a ton of new stuff (all of it copiously detailed by Brichter), but the persistence is the one that means the most to me. The effect is that you can leave Tweetie at any point, use another app, then go back to Tweetie, and it’s almost as though you never left. Feels like switching, rather than quitting/relaunching.
Seriously? Adding stored state — the main iPhone design pattern Apple pushes, and the single method we as developers have to compensate users for the lack of multitasking — finally adding this kind of stored state is heralded as the second coming, rather than at last making up for a major long-standing flaw? And yet AT&T cops shit for only now rolling out MMS.

And then we hear the sad story of how the App Store is broken because Iconfactory's Ramp Champ has not been selling well. Ignoring the fact that "not selling well" means the kinds of numbers I'd cheerfully give my left nut for, let's dig a little deeper and see what's up.
The lack of store front exposure combined with a sporadic 3G crashing bug conspired to keep Ramp Champ down for the count.
So here we have (2) they released a faulty product, but that isn't important because (1) Apple didn't give them front page billing. I mean, for fuck's sake, this is inexcusable. This is the fucking Iconfactory we're talking about. Don't you know who they are? Every time I select the wrong history link in Safari and get sent to the main iPhone dev page, there's the little blue Twitterific bird. Apple wants to start spending some of their advertising budget educating iPhone owners as to exactly which gods are developing for their phone. Ignoring them in this way is simply not on.

We also have this from "Talos" in the comments, which I think just about sums up most of the problems with the world today:
We (Iconfactory) spend at least 3840 hours (8 designers for 7 months) on art and design of Ramp Champ. DS Media Labs working on the programming side on and off for 7 months.
Coding as a afterthought. Yeah, what could possibly go wrong.

Monday, September 28, 2009

Flashforward

Kudos to Jack Davenport for being the only Brit actor working in American TV today able to resist the urge to do a silly accent.

Sunday, September 20, 2009

A Quick Twitter Tip

Not aimed at anyone in particular: If you're going to tell me you're too busy to test the app I've been coding for you, you might like to think about not wiring your game achievements and YouTube favourites into your Twitter stream. M'kay?

Tuesday, September 08, 2009

Lexical: the App Store Control Group


I hope you're sitting down, boys and girls, because what I'm about to type will probably rock the foundation of your entire world: it turns out that the iPhone SDK and a few hours of your spare time are NOT a license to print money. I know, shocking isn't it. Sure, deep down we've all known that the good times have been over for a while — I personally realised it was finished when I saw an article about it in the Sunday Times Magazine — but this may still come as a shock to some people, such as my little friends here.

It's been just over a calendar month since Lexical, my first iPhone app, slipped quietly into the App Store. I wrote it in a few weeks after the project I'd been working on for the previous few months ran spectacularly into a brick wall. I really just wanted to prove to myself that I could actually finish and ship a product.

The chart above shows sales of Lexical for these first five weeks of its commercial life. Week one is particularly impressive (comparatively speaking, of course) given that Lexical appeared on a Thursday and so only represents four days of sales. Its 29 sales are more than the total for the following weeks (14, 4, 4 again and 2 copies, respectively) and simply go to re-underline the importance of the 'New Apps' lists in driving sales.

If you followed the link above, you would have probably drawn your own conclusions about why Lexical hasn't been selling very well. The app icon is dull (or as I like to think of it, elegantly simple), there is only a single line description, and the screenshot is particularly uninspiring (although to be fair that captures pretty well the game's visuals). Coupled with this goes the fact that I made no attempt at promoting it elsewhere.

So I guess the take-home is that, yes, App Store success and the riches and fame which go with it are still possible, it's just that, like everything else worth achieving in life, it now takes some serious commitment and effort to make it. Maybe I should have had the random tile-sorting code generate a swear word each time. Rejections are still great marketing tools. Maybe I should push out incremental update after update to keep it near the top of its category list. Whatever, I'm off to work on another couple of iPhone apps right now. Maybe the next one will sell a million copies.

Wednesday, August 05, 2009

Introducing Lexical


Lexical, my first iPhone application, has just been accepted into the App Store. (You can find it here.)

Lexical is an incredibly simple word puzzle: tap to swap letter tiles around, then swipe to highlight words. It was written in about 14 days (after my previous attempt at an iPhone app ran into an unexpected brick wall), and took about as long to get approved.

Actually, following the recent problems with App Store rejections, I was kind of hoping that I'd get the same treatment. From a publicity point of view it's solid marketing gold. Given that dictionaries are now being rejected for containing dirty words, I was thinking of having a few expletives 'accidentally' appear on each board. (Mind you, I don't do any checking when the board's randomly generated, so there's still a chance that will happen every now and again — probably just as someone's grandmother fires up their first game, knowing my luck...)