一昨日、conferenceWithDevelopersのLTで、1行で導入するback gestureの話をしてきました。
LTの内容と、やろうとして間に合わなかったことの紹介を書きます。

1行で導入するback gesture

iPhone 5が発売されて、UINavigationBarbackButtonItemに指が届きにくくなったので、 スワイプして戻るという動作を実装するアプリが増えてきたように思います。

これを実装するにはUIGestureRecognizerを使ってあれこれするのですが、 毎回これを書くのはだるいので、UIViewControllerを拡張して 1行で導入できるようにしたライブラリを書きました。
ISBackGesture

使い方

UIViewControllerbackGestureEnabledというプロパティが追加されているので、 これの値をYESにするだけです。

UIViewController *viewController = [[UIViewController alloc] init];
viewController.backGestureEnabled = YES;

progressも取得出来ます。

float progress = viewController.backProgress;

実装の話

UIViewControllerの拡張はカテゴリで実現していて、ヘッダーは以下のようになっています。

#import <UIKit/UIKit.h>

@interface UIViewController (BackGesture)

@property (nonatomic) BOOL backGestureEnabled;
@property (nonatomic, readonly) float backProgress;

@end

viewDidLoadの実装を書き換えて、backGestureEnabledYESだったら self.viewUIPanGestureRecognizerを追加するという感じになっています。

- (void)startRecognizing
{
    if (!self.isViewLoaded || !self.backGestureEnabled) {
        return;
    }
    
    self.backGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleGesture:)];
    [self.view addGestureRecognizer:self.backGestureRecognizer];
}

スライド

“ハゲ”ってところにSteve Jobsが写ってますが、気にしないでください。

 

UINavigationBarを拡張した話

このライブラリの目指すところは、元々webtronさんが提案していた “Interaction Concept of Swiping to Go Back”を1行で導入できるようにすることでした。
Interaction Concept of Swiping to Go Back

ISBackGestureを拡張して実現できたのですが、1行で導入することに固執したせいで、 プライベートクラスを使った骨の折れる作業が必要だったので、それを紹介します。
(※ 以下に書くことはwebtronさんのコンセプト自体とは無関係です)
(※ 先に紹介したバージョンのISBackGestureではプライベートクラスは利用していません。 )

UINavigationItemButtonViewのクラスの差し替え

UINavigationBarのViewヒエラルキーの中のボタンにあたる部分は、 UINavigationItemButtonViewというプライベートなクラスで実装されています。 デフォルトで表示されるbackButtonItemに戻りゲージをつけるために、 このオブジェクトのクラスを差し替えました。 クラスを差し替えるにはObjective-C Runtime APIにある、object_setClass()を使います。

差し替えるタイミングには色々と候補がありますが、UIViewdidMoveToSuperviewを選びました。 具体的には、didMoveToSuperviewの実装を以下のものに差し替えます。

- (void)_didMoveToSuperview
{
    Class class = NSClassFromString(@"UINavigationItemButtonView");
    if ([self isKindOfClass:class]) {
        object_setClass(self, [ISNavigationItemProgressButtonView class]);
    }
}

ゲージを描画するUINavigationItemButtonViewの実装

UINavigationItemButtonViewのサブクラスを普通に作ることはできないので、 一旦UIViewのサブクラスを作成し、loadの中でスーパークラスを差し替えます。

+ (void)load
{
    @autoreleasepool {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
        Class class = NSClassFromString(@"UINavigationItemButtonView");
        class_setSuperclass([self class], class);
#pragma clang diagnostic pop
    }
}

progressの値に応じて、ゲージを描画します。

- (void)setProgress:(float)progress
{
    UIView *progressView = objc_getAssociatedObject(self, ISProgressViewKey);
    CGRect frame = progressView.frame;
    frame.origin.x = self.frame.size.width * (1.f - progress);
    
    progressView.frame = frame;
    progressView.alpha = pow(progress, 1.2) * .25f + .1f;
}

UINavigationItemButtonViewdrawText:inRect:barStyle:というメソッドの中で タイトルの描画をしているのですが、そのまま描画されてしまうとゲージの奥に隠れてしまうので、 このメソッドでの文字列の描画をキャンセルし、代わりにUILabelをゲージの上に配置します。

- (void)drawText:(NSString *)text inRect:(CGRect)rect barStyle:(UIBarStyle)style
{
    UILabel *label = objc_getAssociatedObject(self, ISLabelKey);
    label.text = text;
}

progressの受け渡し

UIViewControllerのgestureを受け取る箇所でUINavigationItemButtonViewにprogressを渡します。

for (UIView *subview in [self.navigationController.navigationBar subviews]) {
    if ([subview isKindOfClass:[ISNavigationItemProgressButtonView class]]) {
        ISNavigationItemProgressButtonView *progressButtonView = (id)subview;
        if (gestureRecognizer.state == UIGestureRecognizerStateChanged) {
            progressButtonView.progress = self.backProgress;
        } else {
            [UIView animateWithDuration:.3
                             animations:^{
                                 progressButtonView.progress = 0.f;
                             }];
        }
    }
}

elseでは、gestureをやめたときに徐々にゲージが戻るようにしています。

 

以上のような感じで、以下のようなUIが1行で導入できるようになりました。

こちらのバージョンのコードは別のブランチに置いてあります。
ISBackGesture(extended)

CocoaPodsを使う場合には、以下を指定してください。

pod 'ISBackGesture', :git => 'git@github.com:ishkawa/ISBackGesture.git', :commit => '50fe279672256791e61fbb302676bfb6bafec6e0'

 

2つ目に紹介したバージョンではプライベートなクラスに触れてる箇所もあり、
リジェクトされる可能性が高いので、プロダクトへの導入は自己責任でお願いします。