追記
たまにストックされるので追記です。
記事の内容が理解できたら、実際にコードを書く際には、
などを利用したほうが幸せになれるかと思います。
URLSchemeとは
URLSchemeとは、 非常にざっくり 説明すると、皆さんには見慣れている
http://example.com/
のhttp
の部分です。
iOSアプリでは、このURLSchemeを独自に設定して、そのURLSchemeが呼ばれた時に自身のアプリを開く事ができます。設定方法などは今回説明しないので、適宜ググってください。
はじめから用意されているURLSchemeなどはこちらのドキュメントに書かれています。
URLSchemeで起動した後のお話
さて、独自に設定したURLSchemeが呼ばれると、そのURLSchemeに対応しているアプリが起動すると書きましたが、もう少しプログラムよりな話をするとUIApplicationDelegate
のapplication:openURL:sourceApplication:annotation
が呼ばれます。
実際のフローなどは以下のドキュメントのImplementing Custom URL Schemes
の項に書いてあります。
ここで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を受け取る処理
- (BOOL)openURL:(NSURL *)url;
こんな感じでNSURL
を渡すと、そのURLを元に処理を試みて、その結果(成功or失敗)をBOOL
で返します。
BOOL
で返すようにするのはUIApplicationDelegate
のapplication: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部はNSURL
のhost
で受け取れることは前述のとおりですが、
そのcontroller文字列を元に対応するcontrollerクラスを呼び出さないとなりません。
ここのアプローチとしては
NSString *controllerStr = [url host];
if (controllerStr isEqualToString:@"setting") {
SettingController *controller = [[SettingController alloc] init];
// controllerに対して何か処理をする。
}
というのがパッと浮かびますが、コントローラーを追加するたびに分岐が増えてしまいますので、
controllerクラスのインタフェースを統一しつつ、文字列から直接Class
を取得しnew
するアプローチを取ってみます。
まず、すべてのControllerクラスが実装しなくてはならないプロトコルを定義します。スーパークラスを用意して、すべてサブクラスにするやり方でもどっちでもいいです。
@protocol ControllerProtocol <NSObject>
- (BOOL)action:(NSString *)action query:(NSDictionary *)query;
@end
action:query
については後ほど説明します。
各Controllerはこのプロトコルを実装するようにします。
今回はAlertを表示するだけのAlertControllerを作ってみましょう。
#import "ControllerProtocol.h"
@interface AlertController : NSObject<ControllerProtocol>
@end
@implementation AlertController
- (BOOL)action:(NSString *)action query:(NSDictionary *)query
{
//あとで
return NO;
}
今はaction:query
を実装した、ということが大切です。
さて、これでopenURL
は以下のようにシンプルな状態に保つことがでみます。
- (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
をプロジェクトに追加すればいいだけです。
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エンコードが必要になるかと思います)
実際には以下のようにしました。
- (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を出す
下の2つを補足すると、自分自身のURLSchemeを呼ぶこともできるので、
URLSchemeを利用してアプリ内で遷移させたり出来ます!ということです。
APIのレスポンスによって次に表示するページを切り替えたり、部分的にUIWebViewを利用したりするアプリで、
もしかしたら役に立つかもしれないですね!!
ただし
[[UIApplication sharedApplication] openURL:urlScheme];
こんな感じで自分自身を呼ぶと実行までに時間がかかってしまうので(理由はドキュメント読むとわかる)
[[Routes new] openURL:urlScheme];
と、書いてしまうことをおすすめします。
UIWebView内からのリンクは仕方ないですが<a href={urlscheme}>オリジナルのURLScheme</a>
と書くとUIApplicationDelegate
のapplication:openURL:sourceApplication:annotation
が呼ばれます。