Holko.pl

Hi! I'm Arkadiusz Holko, an independent software developer from Poland. Most recently I've created Outread for iOS.

Follow me on: Feel free to email me.

Interactive Pop Gesture with Custom Back Button or Hidden Navigation Bar

In iOS 7 Apple introduced a new system-wide gesture for popping items from the UINavigationController's stack. It brings iOS closer to Android in this aspect of usability, but it's not straightforward from the developers' perspective. There're two situations in which the gesture stops being recognized:

  • when the default backBarButtonItem is replaced with a custom one
  • when the navigation controller's navigationBar is hidden

A quick Google search reveals a few posts advising to set interactivePopGestureRecognizer.delegate = self and calling it a day. But this solution is actually not bulletproof. During beta tests of Outread we noticed two problems with it:

  • swiping back when a view controller is being pushed can cause a weird UI behavior (e.g. a completely missing back button); quick fix to this problem is already presented at keighl
  • swiping back repeatedly can cause the gesture to be recognized when there's only one view controller on the stack, which in turn puts a UI in a state where it stops recognizing any gestures

Here's a complete solution to both of these problems in a form of the UINavigationController subclass:

# AHKNavigationViewController.h
@interface AHKNavigationViewController : UINavigationController
@end

# AHKNavigationViewController.m
@interface AHKNavigationViewController () <UINavigationControllerDelegate, UIGestureRecognizerDelegate>
@property (nonatomic, getter = isPushingViewController) BOOL pushingViewController;
@end

@implementation AHKNavigationViewController

#pragma mark - NSObject

- (void)dealloc
{
    self.delegate = nil;
    self.interactivePopGestureRecognizer.delegate = nil;
}

#pragma mark - UIViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    __weak typeof(self) weakSelf = self;
    self.delegate = weakSelf;
    self.interactivePopGestureRecognizer.delegate = weakSelf;
}

#pragma mark - UINavigationController

- (void)pushViewController:(UIViewController *)viewController
                  animated:(BOOL)animated
{
    self.pushingViewController = YES;
    [super pushViewController:viewController animated:animated];
}

#pragma mark UINavigationControllerDelegate

- (void)navigationController:(UINavigationController *)navigationController
       didShowViewController:(UIViewController *)viewController
                    animated:(BOOL)animated
{
    self.pushingViewController = NO;
}

#pragma mark - UIGestureRecognizerDelegate

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
    if (gestureRecognizer == self.interactivePopGestureRecognizer) {
        // Disable pop gesture in two situations:
        // 1) when the pop animation is in progress
        // 2) when user swipes quickly a couple of times and animations don't have time to be performed
        return [self.viewControllers count] > 1 && !self.isPushingViewController;
    } else {
        // default value
        return YES;
    }
}

@end

It's the simplest way of fixing these issues that I could come up with. It has one important disadvantage, though. It sets the navigation controller's delegate to self. However, it can be improved by adding automatic forwarding of delegate methods as described in Fixing UITextView on iOS 7 by Peter Steinberger.

Hiding Navigation Bar and Status Bar with Animation on iOS 7

In iOS 7 Apple changed the way a status bar is laid out in the view hierarchy. Mainly, UINavigationController's navigationBar is pinned to the status bar, so when the status bar is hidden the position of the navigation bar changes. It means that when you want to simultaneously hide both these elements you have to perform the operations in the correct order.

Here's how you can do that:

@interface ViewController()
@property (nonatomic, getter = isStatusBarHidden) BOOL statusBarHidden;
@end

@implementation ViewController

#pragma mark - UIViewController

- (BOOL)prefersStatusBarHidden
{
    return self.isStatusBarHidden;
}

#pragma mark - Public

- (void)toggleNavigationBarAndStatusBarVisibility
{
    BOOL willShow = self.navigationController.navigationBarHidden;

    if (willShow) {
        [self toggleStatusBarHiddenWithAppearanceUpdate:NO];
        [self toggleNavigationBarHiddenAnimated:YES];
    } else {
        [self toggleNavigationBarHiddenAnimated:YES];
        [self toggleStatusBarHiddenWithAppearanceUpdate:YES];
    }
}

#pragma mark - Private

- (void)toggleStatusBarHiddenWithAppearanceUpdate:(BOOL)updateAppearance
{
    self.statusBarHidden = !self.isStatusBarHidden;

    if (updateAppearance) {
        [UIView animateWithDuration:UINavigationControllerHideShowBarDuration animations:^{
            [self setNeedsStatusBarAppearanceUpdate];
        }];
    }
}

- (void)toggleNavigationBarHiddenAnimated:(BOOL)animated
{
    [self.navigationController
     setNavigationBarHidden:!self.navigationController.navigationBarHidden
     animated:animated];
}

@end

And here it is in action. You can also change the default status bar animation by implementing preferredStatusBarUpdateAnimation.

Bug Reports

I've posted five radars in the last couple of days:

UISlider's maximumTrackTintColor ignored when set in the Storyboard ( rdar://15771932)

It seems to be a regression in iOS 7.1 (beta). I use it in Outread in a subclass of UISlider for adding vertical marks with a correct color.

Excessive memory usage by UITextView on iOS 7 (rdar://15799494)

This one is a real drag. Text Kit is a great addition to iOS, but UITextView still feels like a 1.0 product (even under iOS 7.1 beta).

UITextField's clear button has wrong vertical position when enclosed in UIAlertView (rdar://15803204)

Not a big bug, but after some time gets annoying.

Buttons on the left side of the screen don't receive UIControlEventTouchDown when custom interactivePopGestureRecognizer delegate is used (rdar://15803254)

It took me some time to debug this issue, so I think it was worth reporting. As a reminder: default delegate of interactivePopGestureRecognizer stops recognizing gestures when a custom leftBarButtonItem is used or when navigationBar is hidden. It's quite common situation.

Read access to Safari Reading List (SSReadingList) (rdar://15804079)

As with Calendar and Reminders, there should be a read and write access to Safari's Reading List.

Hello World!

Welcome to my new blog. I'm going to be posting mainly about iOS and OS X development, as that's what I'm into these days.