Objective Sea

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

Wrestling with Status Bars and Navigation Bars on iOS 7 »

An excellent rundown of all the gotchas when dealing with the new status bar and navigationbar underlaps

NSURL Component Visualization »

I made a thing for when you are working with NSURL and you can’t remember the difference between the `-(NSString *)query` and the `-(NSString *)parameterString`. Click the method name, the associated component is highlighted in red in the URL at the top of the page.

Open the CocoaPod bay door, iPhone!

I’ve just released an iOS app for CocoaPods users. The app, Podlife, is designed to help you keep your pods up-to-date, find new libraries to make building apps faster, and give you quick access to documentation while on the go.

Podlife is free, with a single optional in-app-purchase, so if you have a moment please check it out and see if you find it useful. There is a contact button in the app, and I’d love to hear feedback and comments!

Check it out!

Open the CocoaPod bay door, iPhone!

I’ve just released an iOS app for CocoaPods users. The app, Podlife, is designed to help you keep your pods up-to-date, find new libraries to make building apps faster, and give you quick access to documentation while on the go.

Podlife is free, with a single optional in-app-purchase, so if you have a moment please check it out and see if you find it useful. There is a contact button in the app, and I’d love to hear feedback and comments!

Check it out!

Adding a footer to UIWebView »

So I wrote a pretty cool addition to DMAFWebViewController.

If you set webViewController.webView.footerView to a UIView, the view will be placed inside the webView’s scroll view below all the web content. If the web content changes size (because a page loads) the footer will be pushed below the web content.

I think this may be a novel way of doing this. Googling around for a way to add a footer to UIWebView I found UIWebView with Header and Footer which uses javascript calls to ask the webview how large its content is.

I also used objc_setAssociatedObject to store the content size. You need to remember the last contentSize because you need to adjust the contentSize in order to make room for the footer. You’d get infinite recursion if you didn’t watch for this. The reality is I could easily have used a property to do this, I was just keen to try out the whole associatedObject thing.

The above linked commit details all the changes needed to make this happen.

EDIT: Pages that are shorter than the UIWebView’s frame don’t trigger a contentSize change. Adding this to layoutSubviews fixes the issue by triggering one manually the first time layoutSubviews is called:

if (!objc_getAssociatedObject(self.scrollView, "associated_height")) {
    NSValue *value = [NSValue valueWithCGSize:self.scrollView.contentSize];
    [self observeValueForKeyPath:@"contentSize" ofObject:self.scrollView change:@{NSKeyValueChangeNewKey : value} context:nil];
}

See this commit for the change in action.

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.

wopen 0.2

I’ve updated my wopen bash function to open the .xcodeproj file if no .xcworkspace is available.

Copy the alias from here https://gist.github.com/coryalder/5609996

Place in your .bash_profile or .bashrc file (~/.bash_profile).

Example usage:

cd projectdirectory
wopen

issues open projectdirectory.xcworkspace if that file exists, or open projectdirectory.xcodeproj if that file exists, and errors out nicely if neither do.

PhotoDog

My new app PhotoDog was approved late last night! I’ve spent much of the day sending out press releases, and organizing my social media presence (Instagram and Twitter accounts, mostly). I won’t write much about that here, as this blog is mostly for technical details, but one thing I’ve very proud of is that this marks the first production app that uses my UIActivity for Instagram. Presumably someone else has used it, it’s got a respectable number of stars and forks on GitHub, but this is the first App Store app using it that I know of.

PhotoDog screen shot 1PhotoDog Screenshot showing DMActivityInstagram

I’ll be glad to see it get some battle-testing. :)

CocoaPods tip #0912: faster .xcworkspace opening

UPDATE: 0.2 available here http://objectivesea.tumblr.com/post/50874228125/wopen-0-2

My typical CocoaPods workflow:

  1. Switch to terminal
  2. cd to your project directory
  3. Edit Podfile
  4. type pod update
  5. Quickly Cmd-tab back to Xcode to close the project before CocoaPods rewrites it
  6. Type open .xcodeworkspace
  7. Resume working

The step that trips me up the most of all of these, is ironically step #6. xcw is a hard sequence of letters to type at the best of times.

Solution! wopen

Put this in your .bash_profile or .bashrc:

function wopen {
    open ${PWD##*/}.xcworkspace
} # open Xcode workspace

Now typing wopen will open the .xcworkspace file of the same name as the current directory.

Typical usage: pod update; wopen

Best Practices: UIButtons and their IBAction/IBOutlets

Declare the UIButton outlet:

@property (weak, nonatomic) IBOutlet UIButton *cancelButton;

Declare the action method:

-(IBAction)cancelButtonAction:(id)sender;

Now when you are writing code you will never confuse the outlet and the action. You will always know what this means:

[self cancelButton];

or even worse:

self.cancelButton;

It’s a simple, easy to remember best practice. If you don’t follow this, or a similar convention (cancelButton/cancelAction) it WILL bite you in the ass one day.

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:.