Custom URL Schemeの処理をシンプルに書く

  • 295
    いいね
  • 2
    コメント
この記事は最終更新日から1年以上が経過しています。

追記 

たまにストックされるので追記です。

記事の内容が理解できたら、実際にコードを書く際には、

https://github.com/joeldev/JLRoutes

などを利用したほうが幸せになれるかと思います。

URLSchemeとは

URLSchemeとは、 非常にざっくり 説明すると、皆さんには見慣れている

http://example.com/

httpの部分です。

iOSアプリでは、このURLSchemeを独自に設定して、そのURLSchemeが呼ばれた時に自身のアプリを開く事ができます。設定方法などは今回説明しないので、適宜ググってください。

はじめから用意されているURLSchemeなどはこちらのドキュメントに書かれています。

About Apple URL Schemes

URLSchemeで起動した後のお話

さて、独自に設定したURLSchemeが呼ばれると、そのURLSchemeに対応しているアプリが起動すると書きましたが、もう少しプログラムよりな話をするとUIApplicationDelegateapplication:openURL:sourceApplication:annotationが呼ばれます。

実際のフローなどは以下のドキュメントのImplementing Custom URL Schemesの項に書いてあります。

Advanced App Tricks

ここでURLをアプリ側で受け取る事ができますので、必要ならば渡されたURLを元に処理を行っていくことになります。(なにもしなくてもアプリは起動します)

実際にURLを受け取った部分のソースを上のドキュメントから拝借させて頂きますが、以下の様な感じになります。

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url
        sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
    if ([[url scheme] isEqualToString:@"todolist"]) {
        ToDoItem *item = [[ToDoItem alloc] init];
        NSString *taskName = [url query];
        if (!taskName || ![self isValidTaskString:taskName]) { // must have a task name
            return NO;
        }
        taskName = [taskName stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];

        item.toDoTask = taskName;
        NSString *dateString = [url fragment];
        if (!dateString || [dateString isEqualToString:@"today"]) {
            item.dateDue = [NSDate date];
        } else {
            if (![self isValidDateString:dateString]) {
                return NO;
            }
            // format: yyyymmddhhmm (24-hour clock)
            NSString *curStr = [dateString substringWithRange:NSMakeRange(0, 4)];
            NSInteger yeardigit = [curStr integerValue];
            curStr = [dateString substringWithRange:NSMakeRange(4, 2)];
            NSInteger monthdigit = [curStr integerValue];
            curStr = [dateString substringWithRange:NSMakeRange(6, 2)];
            NSInteger daydigit = [curStr integerValue];
            curStr = [dateString substringWithRange:NSMakeRange(8, 2)];
            NSInteger hourdigit = [curStr integerValue];
            curStr = [dateString substringWithRange:NSMakeRange(10, 2)];
            NSInteger minutedigit = [curStr integerValue];

            NSDateComponents *dateComps = [[NSDateComponents alloc] init];
            [dateComps setYear:yeardigit];
            [dateComps setMonth:monthdigit];
            [dateComps setDay:daydigit];
            [dateComps setHour:hourdigit];
            [dateComps setMinute:minutedigit];
            NSCalendar *calendar = [s[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
            NSDate *itemDate = [calendar dateFromComponents:dateComps];
            if (!itemDate) {
                return NO;
            }
            item.dateDue = itemDate;
        }

        [(NSMutableArray *)self.list addObject:item];
        return YES;
    }
    return NO;
}

この例だとURLSchemeはtodolistで、todolist://?taskName#todayこんなURLを受け取ることを想定していて(多分)taskNameというtodoをtoday(今日)の日付で作成して登録しています。

こんな感じで、受け取るURLによって様々な処理をさせることが出来ます。

この処理を書く部分ですが、例の様にAppDelegateにだらだら書いてしまいがちです。

処理の種類が少ないうちはそれでも対応できますが、種類が増えて分岐が増えてくるとあっという間にスパゲッティな感じになりそうです。AppDelegateはできればシンプルを保ちたいですね。

それでは どうやってこの部分をシンプルに保つのか 。というのがこの記事のメインTopicです。

どのように処理をシンプルにするのか

偉そうに書いて居ますが、世の中のWEBアプリケーションフレームワークのURL設計を参考に真似すればいいだけです\(^o^)/

記事の最後にサンプルプロジェクトを置いておくので、記事中の説明は必要箇所だけにします。

あくまでも一例です。

URLの設計をする

URLを作る上でのルールですね、各アプリケーションで決めればいいと思いますが大体こんな感じで足りるのでは無いでしょうか。

scheme://controller/action?query

上記のURLはNSURLにすると

scheme = [url scheme]
controller = [url host]
action = [url lastPathComponent]
query = [url query]

こういう感じでそれぞれ受け取れるので楽です。

アプリ側は受け取ったcontrollerを元に、使用するcontrollerクラスを選び、そのcontrollerクラスにactionとqueryを渡します。各actionの挙動については各々のcontrollerに任せるという実装にしてみます。

実際の処理の流れ

まずは、URLを元にcontrollerを呼び出す処理をさせるクラスを作ります。ここでは半端なRailsの知識からRoutesクラスと名づけましょう。(意味違ったらこっそり教えて下さい)

以降、説明の都合上Prefixがないクラスを作っていきますが、実際にはちゃんとつけてあげてください。

URLを受け取る処理

Routes.h
- (BOOL)openURL:(NSURL *)url;

こんな感じでNSURLを渡すと、そのURLを元に処理を試みて、その結果(成功or失敗)をBOOLで返します。

BOOLで返すようにするのはUIApplicationDelegateapplication:openURL:sourceApplication:annotationの返り値もBOOLなので極限にシンプルにすると

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url
        sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
    return [routes openURL:url];
}

こんな感じで書けるからです。実際にはscheme名くらい見たほうが後々楽そうなので、

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url
        sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
    if ([[url scheme] isEqualToString:@"myapp"]) {
        return [routes openURL:url];
    }
    return NO;
}

こんな感じの落とし所になるでしょうか。

controllerを呼び出す

さて、URLのcontroller部はNSURLhostで受け取れることは前述のとおりですが、
そのcontroller文字列を元に対応するcontrollerクラスを呼び出さないとなりません。

ここのアプローチとしては

NSString *controllerStr = [url host];
if (controllerStr isEqualToString:@"setting") {
    SettingController *controller = [[SettingController alloc] init];
    // controllerに対して何か処理をする。
}

というのがパッと浮かびますが、コントローラーを追加するたびに分岐が増えてしまいますので、
controllerクラスのインタフェースを統一しつつ、文字列から直接Classを取得しnewするアプローチを取ってみます。

まず、すべてのControllerクラスが実装しなくてはならないプロトコルを定義します。スーパークラスを用意して、すべてサブクラスにするやり方でもどっちでもいいです。

ControllerProtocol.h
@protocol ControllerProtocol <NSObject>
- (BOOL)action:(NSString *)action query:(NSDictionary *)query;
@end

action:queryについては後ほど説明します。
各Controllerはこのプロトコルを実装するようにします。

今回はAlertを表示するだけのAlertControllerを作ってみましょう。

AlertController.h
#import "ControllerProtocol.h"
@interface AlertController : NSObject<ControllerProtocol>
@end
AlertController.m
@implementation AlertController
- (BOOL)action:(NSString *)action query:(NSDictionary *)query
{
    //あとで
    return NO;
}

今はaction:queryを実装した、ということが大切です。

さて、これでopenURLは以下のようにシンプルな状態に保つことがでみます。

Routes.m
- (BOOL)openURL:(NSURL *)url
{
    NSString *controllerName = [url host];

    // 先頭の一文字を大文字にする
    controllerName = [controllerName stringByReplacingCharactersInRange:NSMakeRange(0,1)
                                                             withString:[[controllerName substringToIndex:1] capitalizedString]];

    // Classを文字列から取得
    Class controllerClass = NSClassFromString([NSString stringWithFormat:@"%@Controller",controllerName]);

    // ClassがControllerProtocolを実装しているかチェック
    if ([controllerClass conformsToProtocol:@protocol(ControllerProtocol)]) {
        id<ControllerProtocol> controller = [[controllerClass alloc] init];
        return [controller action:[url lastPathComponent] query:[self _dictionaryFromQueryString:[url query]]];
    }
    return NO;
}

_dictionaryFromQueryString:の部分はURLについてくるtitle=titleString&message=messageStringなどのquery部分を後で利用しやすいように

@{
    @"title":@"titleString",
    @"message":@"messageString"
}

といったNSDictionaryに変換しているだけです。

他の処理内容はコメントで察してもらうとして、一箇所だけ。

// ClassがControllerProtocolを実装しているかチェック
if ([controllerClass conformsToProtocol:@protocol(ControllerProtocol)])
{
}

この部分は、呼びだそうとしているクラスが本当にControllerProtocolを実装しているControllerクラスかどうかをチェックしています。

URLはアプリ外部から渡されるものなので、悪意のある文字列が渡される可能性があります。つまりセキュリティリスクになりうるので、このように外部から得たデータを使用する時は 特に注意が必要 です。

セキュリティについては

Validating Input and Interprocess Communication

こちらのドキュメントも一読ください。

とりあえずこれでmyapp://alert/fugaというURLを呼ぶと、AlertControllerが存在した場合、インスタンス化してaction:queryを呼び出します。actionには@"fuga"が入りますね。

Controllerを追加したい場合はControllerProtocolを実装したControllerClassをプロジェクトに追加すればいいだけです。

appDelegateif祭りにしなくて済みます!

controllerにactionを実装する。

さきほど、AlertControllerを作ったと思うので、その中に処理を書いてみます。

受け取った文字をUIAlertViewで表示するだけのシンプルなactionです。showアクションとしてみましょう。

このアプリのURLSchemeをmyappだとするとAlertControllershowアクションを呼ぶためのURLはmyapp://alert/showとなり、showアクションではアラートのタイトルとメッセージも受け取りたいので最終的には

myapp://alert/show?title=titleStr&message=messageStr

この様になると良さそうです。(通常queryなどはURLエンコードが必要になるかと思います)

実際には以下のようにしました。

AlertController.m
- (BOOL)action:(NSString *)action query:(NSDictionary *)query
{
    SEL sel = NSSelectorFromString([NSString stringWithFormat:@"%@Action:", action]);
    if ([self respondsToSelector:sel]) {
        BOOL (*actionImp)(id, SEL, NSDictionary *);
        actionImp = (BOOL (*)(id, SEL, NSDictionary *))[self methodForSelector:sel];
        return actionImp(self, sel, query);
    }
    return NO;
}

- (BOOL)showAction:(NSDictionary *)query
{
    if (query[@"title"] && query[@"message"]) {
        [[[UIAlertView alloc] initWithTitle:query[@"title"] message:query[@"message"] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil, nil]show];
        return YES;
    }
    return NO;
}

action:query内でactionを元にSEL(@selector(showAction:))を取得して、存在すればそれを実行しています。
performSelector:withObject:を使わずに関数を直に実行しているのは、戻り値としてBOOLが欲しいからです。

いまさらですが、action:queryは各Controllerクラスで共通になりそうなので、ControllerProtocolではなくControllerBaseクラスを作ってそれを継承する方が良さそうでしたね。

そうすれば、各ControllerがhogeAction:を実装するだけで良くなり、もう少しシンプルになります。

話を戻しますが以上でmyapp://alert/show?title=title&message=messageが呼ばれた際に、AlertContorllershowAction:を呼ぶようにできました。

あとは同じようにControllerを増やしたり、Actionを増やしたりしていけばいいだけです。

非常にシンプルですね!

以上、Custom URL Schemeの処理をシンプルに書く。でした!

それではクリスマスに向けて、快適なURLScheme生活を送ってくださいね〜。

注意点

  • URLSchemeは他のアプリとかぶることが有ります。
  • くれぐれもセキュリティには気をつけてください。

おまけ

サンプルコードに以下のサンプルが入ってます。

カスタムURLSchemeを便利に使って行きましょう。

  • ActionControllerのshowAction
  • アプリ内or外からURLSchemeでpresentViewControllerを出す
  • UIWebView内からURLSchemeでpresentViewControllerを出す

下の2つを補足すると、自分自身のURLSchemeを呼ぶこともできるので、
URLSchemeを利用してアプリ内で遷移させたり出来ます!ということです。

APIのレスポンスによって次に表示するページを切り替えたり、部分的にUIWebViewを利用したりするアプリで、
もしかしたら役に立つかもしれないですね!!

ただし

[[UIApplication sharedApplication] openURL:urlScheme];

こんな感じで自分自身を呼ぶと実行までに時間がかかってしまうので(理由はドキュメント読むとわかる)

[[Routes new] openURL:urlScheme];

と、書いてしまうことをおすすめします。

UIWebView内からのリンクは仕方ないですが<a href={urlscheme}>オリジナルのURLScheme</a>と書くとUIApplicationDelegateapplication:openURL:sourceApplication:annotationが呼ばれます。


サンプル

CustomURLSchemeSample