Objective Sea

Sailing on the Objective-C sea, see?
Written by Cory, from Davander Mobile.
@corydmc on twitter, or email me objectivesea@davander.com

#iPhone

Put a UISlider into a Popover

To clean up the UI of my app Chack! I decided to move the UISlider from the toolbar to a popover coming from a toolbar button.

Before:

Chack!'s current UI. UISlider in the UIToolbar

After:

After adding UISlider in a UIPopover

There’s more going on than just the change to the UISlider, but the key part of this is there used to be a UISlider in the toolbar, and now it’s accessed via a button on the toolbar. This leaves room for another button, which I’ve used to add a proper color picker. It means more taps for the user, but overall I believe it’s an improvement in control and usability.

To do this change, I tried a bunch of things. The first thing I did was try UIPopoverController. I was quickly reminded that this only works on iPad, not on iPhone. sad trombone.

So the next step is always a trip to cocoacontrols.com and cocoapods.org to look up a viable replacement. If I can’t find something there, I generally settle in to build it myself, but usually something already exists that can be adapted.

After auditioning a few popover classes, I settled on PopoverView. I tried WEPopover first, but I ran into some problems where the popover was making my UIBarButtonItem disappear every time I showed it. I never sorted out the issue, but PopoverView works well enough, and looks great. The only downside is that it doesn’t have a present-from-bar-button-item method on it.

pod 'PopoverView', '~> 0.0.1'

You are using CocoaPods, right? Add the above line to your Podfile and run pod update to install it. If you’re not using cocoapods already, HEAVEN HELP YOU, YOUR SOUL IS LOST!

Once installed, go to your view controller and add a custom UIView to your nib, outside of your main view. Place a single UISlider in the center of the view. It’s going to be rotated to be vertical, so you want to make it as wide as the containing view is long (less 20px margins on either side). Since my view is 270px tall, I made my UISlider 230px wide and centered it.

UISlider in a custom UIView

Xcode doesn’t offer a way to visually make a vertical slider, but you can do it in the .xib by using User Defined Runtime Attributes. Go to that section, and set the Key Path to layer.transform.rotation.z, the type to String and the value to -1.570795 (which is -pi/2).

UISlider rotated 90 degrees

Now wire up the UIBarButtonItem to a IBAction method on your view controller (sliderButtonAction:), and wire up the UIView that contains the UISlider to an IBOutlet (sliderView). Don’t forget to #import "PopoverView.h".

-(IBAction)sliderButtonAction:(id)sender {
    UIView *topView = nil;
    NSArray* windowViews = [[[UIApplication sharedApplication] keyWindow] subviews];
    for (int i = windowViews.count - 1; i >= 0 ; i--) {
        topView = [windowViews objectAtIndex:i];
        if (!topView.isHidden) break;
    }
    [PopoverView showPopoverAtPoint:(CGPoint){147.0f, topView.frame.size.height-44.0f} inView:topView withContentView:self.sliderView delegate:nil];
}

The first bit of this method finds the topmost view, and the second bit uses the topmost view to present the popover. A major downside to PopoverView, as mentioned earlier, is that you have to specify the location of the popover’s presenter. This means either hard-coding it, as I’ve done, or writing some code (as WEPopover has done) to find the location programmatically. I chose the easy way out, and just fudged the location. Annoying if I change the button layout ever, but it works for now.

And amazingly, that’s it. PopoverView handles hiding and showing the popover, and prevents two popovers from showing at once (grounds for app store rejection, I know from personal experience). You need to wire the slider up to your UI to do whatever it’s supposed to do, but that’s the basic premise.

AFNetworking - Cancelling operations

I recently ran into trouble trying to cancel operations in AFNetworking.

Two issues came up. The first was finding my previous query, to cancel it. The second was receiving some kind of notification that the operation was cancelled. The failure block isn’t fired when operations are cancelled.

Since my app uses GET requests, cancelAllHTTPOperationsWithMethod:path: was failing to find my operation. It was looking for a full path, query string and all. Since the query parameters are off somewhere else in my app, it was much easier to just override this method, and write code to match just the URL, ignoring the query string. Along the way, I improved on this StackOverflow question regarding trimming the query string off an NSURL.

The next bit required digging into the GitHub issues for AFNetworking to resolve. The default behaviour of AFNetworking is to call the failure block if the HTTP request fails, the success block if it succeeds, and do nothing if you cancel it. The logic is that if you are cancelling it, you can probably deal with the cancellation however you want. Details are available in AFNetworking/Issue#479. The workaround for this is to set the completionBlock property manually, instead of using setCompletionBlockWithSuccess:failure:.

Tip #293 for writing good Objective-C methods: Fail Early

Do this:

- (id)someObject:(DMSomeObject *)caller didSelectView:(DMObjectView *)view {
    if (caller != self.keyObject) return nil;
    ***<LONG METHOD REACTING TO A SELECTED VIEW, RETURNING AN OBJECT***
}

Don’t do this:

- (id)someObject:(DMSomeObject *)caller didSelectView:(DMObjectView *)view {
    if (caller == self.keyObject) {
        ***LONG METHOD REACTING TO A SELECTED VIEW, RETURNING AN OBJECT****
    }
    return nil;
}

Deeply nested if statements, coupled with blocks based apis or enumeration loops will have you lost in your own code. When possible, fail early for simpler code.

App Store Spam - 28 identical apps

In August, iOS developer George Talusan put 22 identical copies of the same app in the App Store. That’s 22 times, under 22 different names.

When I discovered this, a few months ago, I reported it to Apple immediately. Since then, he has put the same app in another 6 times, with a slightly modified UI. Apple does not seem to have responded.

I stumbled on this honeypot of apps while looking for a white noise generator during a bout of insomnia. The screenshot looked attractive and it was free, so I downloaded it. I’ve tried a few other noise generator apps, and this one was definitely not a good one. The sounds were alright, but it stopped playing sound the moment my phone went to sleep. Most other noise generators will continue playing. That said, even if this app was god’s gift to iPhones, it wouldn’t excuse the 28 copies spamming the App store and Clogging up your search results.

(Three variations on the one UI, depending on when this duplicate was last updated)

Some of the copies of the app are free, and some of them are $0.99 or $1.99. They all seem to have an in-app purchase to unlock the full feature set.

He’s since fixed the issue of background audio in some, but not all, of the duplicates. I’m sure it’s hard work keeping 28 apps up to date.

It’s in the store under the names Virtual Earplugs, Ambient Soundscapes, Power Nap - Soundscapes, iEarplugs, Noise Block, Brain Tuner - Focus, Nap+, iRelax - Soundscapes, Napbot, Meditator, Mood Tweak, Toga Sound, Dreambot, Babywaves, Hushbaby, iDream -Sleep Maker, iPacifier, SleepAid - Soundscapes, Mood Mod, Tuneout - Sound Blocker, Soundwaves, and Focus+. In December he added Soundscaper, Soundscaper Pro, Sound Oasis, Sound Oasis Pro, Sleep Maker and Sleep Maker Pro.

These apps are listed in the categories: Utilities, Productivity, Travel, Business, Medical, Lifestyle, Heathcare & Fitness, and Education.

This is a pretty big failure in the app approval process, and I’d be willing to bet it’s not an isolated incident. Each of these has been looked at by a real human being, who no doubt had access to their account history. It doesn’t take a genius to spot that these apps are all IDENTICAL. From what I can see without buying them all the only thing that varies between them is the icon, the name, and the app description. Either they have no directive to stop stuff like this from happening, or they have no system in place to catch it.

I’d love to hear about other examples of this kind of App Store SEO/spam.

Deretina.py - never worry about Retina graphics again

(or rather, never worry about non-retina graphics again)

I’ve devised a system to stop having to fiddle with retina and non-retina graphics when building iOS apps.

  1. Only make retina (@2x) graphics assets.
  2. Store them all in an “Assets” directory, added to my xcode project as a folder-reference.
  3. Add a “Run Script” build step that converts any and all @2x graphics to half-rez graphics for older devices.

The script can be found here. It crawls a sub-directory of your project called “Assets”, so any @2x files in there will be shrunk effortlessly using OS X’s built in sips command line tool.

I’ve only been using this for a day or two in one project, so I’d love to hear of any issues you encounter. Or fork it on github and improve it.

deretina.py

The special cruelty of Lodsys

The thing that gets me riled up the most about this lodsys business is that they’re going after the little guys. Look at this comparison:

Companies sued by Lodsys: Combay, Iconfactory, Illusion Labs, Shovelmate, Quickoffice, Richard Shinderman, and Wulven Game Studios.

Companies making crazy-money off in-app purchase: Zynga, Pocket Gems, Capcom, The Playforge, SEGA, Haypi Co, Z2Live, KAMAGAMES, etc.

I understand that this is strategic. Those bigger companies would challenge Lodsys’ claims, and it would be years before a settlement or decision was made. Further, if you pick on a few little guys you scare the rest of them into compliance.

But it just hurts to see this happen to small companies, because many of them will be forced to settle because they don’t have the money to defend themselves. The moral and legal soundness of Lodsys’ case will not be the deciding factor in most companies decision to pay or fight.

I’ve been planning to add in-app purchase to CatPaint for some time now. For now I’ll be waiting to see how this plays out.

Elance posting requesting a clone of CatPaint (my app). Should I take the job?

Elance posting requesting a clone of CatPaint (my app). Should I take the job?

iPhone Property-List Benchmark

Researching the best way to solve a problem, I decided to write a quick benchmark. The issue involves a complex many-levelled hierarchical dictionary (+20k when stored in XML format), and I wanted to know if crawling it looking for specific keys would be too slow to be viable. My test function crawls the dictionary, and looks for keys that exist (4), and some that don’t (6). I repeat the process 1000 times, for luck.

I wrote the initial test as a command-line app to run on the Mac. Mostly because testing is quicker when you don’t have to deploy to a device. It’s a 2.66Ghz Core 2 Duo.

found 4000 of the 10000 keys in 0.763134 seconds

Then I added my code to a template view-based application for iPhone, and tested in the simulator.

found 4000 of the 10000 keys in 1.249594 seconds

Then I tested on-device (not in debug mode). It’s a 16gb iPhone 4, for the record.

found 4000 of the 10000 keys in 14.157210 seconds

That means Mac : Simulator : iPhone has a speed ratio of 1 : 1.6 : 18.6. Obviously this is not going to be a hard and fast rule, but for this type of operation it seems like a useful rule-of-thumb.

I will probably dig out the rest of my testing suite (iPad 2, iPad 1, 1st Gen iPod Touch, iPhone 3GS) to fill out these stats. I will update this post when I do. For now my question has been answered: yes, this solution will work.

iAd Gallery »

Apple has put an app in the app store that ONLY displays iAds.

I’d just like to state for the record that back when I made my first ad-supported app (Fixed, which I later made $1 because ads are bullshit) I thought about doing exactly this. I’m certain if I had, it would have been rejected immediately.