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

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

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

https://github.com/joeldev/JLRoutes

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

## URLSchemeとは

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

```
http://example.com/
```

の`http`の部分です。

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

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

[About Apple URL Schemes][1]



## URLSchemeで起動した後のお話


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

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

[Advanced App Tricks][2]

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

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

```objectivec
- (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にすると

```objectivec
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を受け取る処理

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

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

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

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

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

```objectivec
- (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部は`NSURL`の`host`で受け取れることは前述のとおりですが、
そのcontroller文字列を元に対応するcontrollerクラスを呼び出さないとなりません。

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

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

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

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


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

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

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


```objectivec:AlertController.h
#import "ControllerProtocol.h"
@interface AlertController : NSObject<ControllerProtocol>
@end
```


```objectivec:AlertController.m
@implementation AlertController
- (BOOL)action:(NSString *)action query:(NSDictionary *)query
{
    //あとで
    return NO;
}
```

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


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


```objectivec: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`部分を後で利用しやすいように

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

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

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

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

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

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

セキュリティについては

[Validating Input and Interprocess Communication][3]

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

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

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

`appDelegate`を`if`祭りにしなくて済みます！

### controllerにactionを実装する。
さきほど、`AlertController`を作ったと思うので、その中に処理を書いてみます。

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

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

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

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

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


```objectivec: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`が呼ばれた際に、`AlertContorller`の`showAction:`を呼ぶようにできました。

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

__非常にシンプルですね！__

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

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

## 注意点

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

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

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

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

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

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

ただし

```objectivec
[[UIApplication sharedApplication] openURL:urlScheme];
```

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

```objectivec
[[Routes new] openURL:urlScheme];
```

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

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

---

## サンプル

[CustomURLSchemeSample][4]


  [1]: https://developer.apple.com/library/ios/featuredarticles/iPhoneURLScheme_Reference/Introduction/Introduction.html
  [2]: https://developer.apple.com/library/ios/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/AdvancedAppTricks/AdvancedAppTricks.html
  [3]: https://developer.apple.com/library/ios/documentation/Security/Conceptual/SecureCodingGuide/Articles/ValidatingInput.html
  [4]: https://github.com/naonya3/CustomURLSchemeSample/tree/master/CustomURLSchemeSample