Saturday, December 14, 2013

iOS: Logo Image in Navigation Bar

One of the most frustrating things about developing for iOS is that you can do powerful things without breaking a sweat (e.g. animation), but sometimes getting the simplest things right involves a painful process of tweaking, scouring the internet and head-banging experimentation.

I recently wanted to display a logo image in the navigation bar of one of my screens. Sounds simple, right? Well it very nearly is, as there's a property self.navigationItem.titleView that is available in your view controller. Create an image view for your logo image, and use it to set this titleView property. Piece of cake.

Well, not quite. Problem is, the logo can end up blurry, and fail to resize correctly when you rotate the iPhone / iPad. And it turns out solving those two issues is not straightforward.

Since I've found the magic formula that makes iOS display a nice, sharp and correctly-sized navigation image on all devices and versions (tested v5 and up), I thought I'd share it. It goes like this:

Navigation Logo


1. Create an image called navigation-logo.png with height of 44 pixels - any width is fine. Create another image called navigation-logo@2x.png with height of 88 pixels - again, any width is fine. Add them to your XCode project.

2. In your view controller's viewDidLoad method, add the following:
- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    UIImageView *imageView = [[UIImageView alloc]
        initWithFrame:CGRectMake(0,0,3,44)];
    imageView.contentMode = UIViewContentModeScaleAspectFill;
    imageView.clipsToBounds = NO;
    imageView.image = [UIImage imageNamed:@"navigation-logo"];
    controller.navigationItem.titleView = imageView;
}

Note we use a width of 3 for the UIImageView. This is important - if it's 2 (or any even number), then the image will be blurry. Setting clipToBounds=NO and contentMode=UIViewContentModeScaleAspectFill means the image will display outside the image view's width (fine), and will be correctly proportioned.

And that's all there is to it. Simple when you know how...

Tuesday, December 3, 2013

Password-Free SSH

One quick tip on getting password-free SSH working: make sure the permissions are correct on your directories. Otherwise, SSH will fallback to prompting you for a password without telling you why.

Today I was trying to set up password-free root ssh access to my Western Digital MyBook external hard drive. Infuriatingly, it kept prompting me for a password... until I found this site containing the key missing step: I had the wrong permissions on my root user's home directory!

So on the box that you are trying to remote into, run these commands to set up the correct file permissions for ssh:
chmod 700 ~
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys 

Friday, November 22, 2013

iOS: Prevent Back Button Navigating to Previous Controller

Sometimes you want to stop your user immediately going back to the previous screen without prompting them first. You could do this by creating a custom back button - drag a button in storyboard to the top left of the navigation bar, and wire it up to your view controller. In your custom back button's selector, show an alert asking the user to confirm. If confirmed, you then manually go back by calling [self popViewControllerAnimated:YES].

One problem with this is that you lose the special left-slanting arrow shape of the back button. Another problem is that other actions that pop the navigation controller will be excluded from this "Are You Really Sure?" check. For example, double-tapping on a tab on your tab bar controller by default pops you to the root view controller.

Hence the need to be able to intercept all attempts to pop the current view controller, and also to be able to prevent the pop from happening.

Turns out this is possible by writing a custom navigation controller. But it is a little tricky to get it right, so I'm putting the code here to save others some time.

Safe Navigation Controller and Delegate


Create a new class UISafeNavigationController as follows:
//
// UISafeNavigationController.h

#import <UIKit/UIKit.h>

@protocol UISafeNavigationDelegate <NSObject>
@required
- (BOOL)navigationController:(UINavigationController *)navigationController
    shouldPopViewController:(UIViewController *)controller pop:(void(^)())pop;
@end

@interface UISafeNavigationController : UINavigationController
@property (weak, nonatomic) id<UISafeNavigationDelegate> safeDelegate;
@end

//
// UISafeNavigationController.m

#import "UISafeNavigationController.h"

@implementation UISafeNavigationController
@end
The safeDelegate will be polled before every pop and given the opportunity to prevent the pop from happening. The pop:(void(^)())pop is passed to the safeDelegate to allow it to trigger the pop to happen at some later point, after it has done whatever checks it needs (e.g. get confirmation from the user via an alert).

Now the main job is to override the pop methods in UISafeNavigationController. Add the following methods to the implementation in the .m file:
- (UIViewController *)popViewControllerAnimated:(BOOL)animated
{
    if (self.safeDelgate && ![self.safeDelegate navigationController:self
        shouldPopViewController:[self.viewControllers lastObject]
        pop:^{ [super popViewControllerAnimated:animated]; }])
    {
        return nil;
    }
    
    return [super popViewControllerAnimated:animated];
}
- (NSArray *)popToRootViewControllerAnimated:(BOOL)animated
{
    if (self.safeDelgate && ![self.safeDelegate navigationController:self
        shouldPopViewController:[self.viewControllers lastObject]
        pop:^{ [super popToRootViewControllerAnimated:animated]; }])
    {
        return nil;
    }
    
    return [super popToRootViewControllerAnimated:animated];
}
- (NSArray *)popToViewController:(UIViewController *)viewController
    animated:(BOOL)animated
{
    if (self.safeDelgate && ![self.safeDelegate navigationController:self
        shouldPopViewController:[self.viewControllers lastObject]
        pop:^{ [super popToViewController:viewController animated:animated]; }])
    {
        return nil;
    }
    
    return [super popToViewController:viewController animated:animated];
}
With the above code, any attempts to pop a view controller will result in a call to the safeDelegate (if it has been set) for confirmation.

Now for the slightly tricky part. We need to stop the navigation bar popping as well - at the moment, it will still pop when you click back, even if we prevent the navigation controller from popping.

To achieve this, make the UISafeNavigationController conform to UINavigationBarDelegate with the following changes in the header file:
@interface UISafeNavigationController : UINavigationController
    <UINavigationBarDelegate>
@property (weak, nonatomic) id<UISafeNavigationDelegate> safeDelegate;

- (BOOL)navigationBar:(UINavigationBar *)navigationBar
    shouldPopItem:(UINavigationItem *)item;
@end
Finally, implement the UINavigationBarDelegate method as follows:
- (BOOL)navigationBar:(UINavigationBar *)navigationBar
    shouldPopItem:(UINavigationItem *)item
{
    if (item == [[self.viewControllers lastObject] navigationItem]) {
        [self popViewControllerAnimated:YES];
        return NO;
    }
    
    return YES;
}
And that's all there is to it! Now all you need to do is change the class of your navigation controller in storyboard to UISafeNavigationController, then for any view controllers that need to prevent the user going back, add UISafeNavigationDelegate to their protocol list and add the following in the implementation:
- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    
    [((UISafeNavigationController *) self.navigationController)
        setSafeDelegate:self];
}
- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    
    [((UISafeNavigationController *) self.navigationController)
        setSafeDelegate:nil];
}
- (BOOL)navigationController:(UINavigationController *)navigationController
    shouldPopViewController:(UIViewController *)controller pop:(void(^)())pop
{
    if (/* Some test for pop */) {
        pop();
        return NO;
    }
    return YES;
}

Like I say, this took me a bit of fiddling to get right, so maybe it'll save someone else the hassle!

iOS: Anonymous Blocks As Method Parameters

Every time I want to write a method that takes a simple block as a parameter, I have to do a google search to get the correct syntax. So I'm putting it here so I'll know where to look next time! Here's an example:
- (void)someMethodWithBoringParameter:(NSInteger)boring
    excitingBlockParameter:(void(^)())callback;
You then call the method like so:
[self someMethodWithBoringParameter:99 excitingBlockParameter:^{
    //
    // Do something funky
    //
}];
Just for completeness, here's how to declare the block as a variable:
void(^excitingBlock)() = ^{
    // Code
};

iOS: Navigation Title Text Colour

To change the titles in all your navigation bars to white, add this code to the didFinishLaunchingWithOptions method in your app delegate:
NSDictionary *navbarTitleTextAttributes =
    @{ UITextAttributeTextColor:[UIColor whiteColor] };
[[UINavigationBar appearance] setTitleTextAttributes:navbarTitleTextAttributes];

iOS7: Scroll to Top

Should be simple right?
self.tableView.contentOffset = CGRectZero;
However, on iOS7 your tableView will end up with its top part hidden under the navigation bar. Instead, you need to do this:
self.tableView.contentOffset = CGPointMake(0, -self.tableView.contentInset.top);

Thursday, November 21, 2013

iOS: Gotchas

Here's a couple of things that tripped me up today while developing an iOS app:

1. Views Blocking Touches

If you drag a plain view onto your view controller in storyboard, it will stop all touches reaching views located underneath. You will need to ensure the User Interaction Enabled setting is unchecked if you want your touches to be passed through.

2. Tap and Drag Location Tracking

Storyboard lets you easily wire up your buttons to methods in your view controller that get called whenever a user taps or drags inside the button. If you use Storyboard to create the stub method for you by dragging in the connection to an empty space in your view controller, it will create a method like:
- (IBAction)editClicked:(id)sender;
However, you may want to know where the user touched. Fortunately, this is easily available - just ensure you select the Sender and Event option in the Arguments drop-down when create the connection in Storyboard. This will then auto-create a method in your header file like:
- (IBAction)editClicked:(id)sender forEvent:(UIEvent *)event;
You can then get the touch position with the following code:
NSSet *touches = [event allTouches];
UITouch *touch = [touches anyObject];
CGPoint pointTouched = [touch locationInView:self.view];