しばらくiOSから離れていてSwiftはほとんど触ったことないのでとりあえずObjective-Cでやりました。このあとSwiftで実装し直そうと思ってるけれど、一旦エントリにします。

いつだったかDHHが既存のWebサイトを活かしてアプリ作るよーみたいなことを言ってたのを覚えていたので、今回は「いや、実際どうやってやるんだろうな」というのを考えてみたわけです。

何をやりたいか

こういうページ遷移がある普通のWebサイトがあるとして、

このページをiOSのUIWebViewで表示するんだけど、リンクをタップしたときに新しいViewControllerを作ってNavigationで遷移するということをやってみます。普通にやるとリンクをタップしたら同一WebView内画面遷移するところを、あたかもiOSのネイティブのように動かしてみようということね。

結論からいうとそれは可能です。↓のアニGIFを見てもらえばリンクを押したときにナビゲーションで次のビューがスタックしてくるのがわかると思います。

こうすることでWebにコンテンツを自由にデプロイしつつ、ユーザー体験はネイティブに近づけられるんじゃないかと。

こんなことはすでに昔からみんながやってたりしたかもしれないけど、最近は端末も速くなっているし、個人的にはWebView+ナビゲーションってあまり見たことがない気がするのでその部分の実験ということで見てもらえると良いです。

Webページの準備

まずはHTML側のソースです。ここで見るべきは、<a>タグについてるcallback-to-iosクラスぐらい。

1<!DOCTYPE html>
2<html>
3  <body>
4    <p>1ページ目</p>
5    <a href="/secondpage.html" class="callback-to-ios">2ページ目ヘ</a>
6  </body>
7</html>

続いてJavascriptです。こっちが肝ね。

 1$ ->
 2  reportBackToIOS = (href)->
 3    iframe = $('<iframe />').attr('src', "callback://" + href)
 4    $('body').append(iframe)
 5    iframe.remove()
 6    iframe = null
 7
 8  $('.callback-to-ios').click ->
 9    reportBackToIOS($(@).attr("href"))
10    false

まずリンクのクリック時にreportBackToIOSを呼び出した上で、falseを返してリンクを辿らないようにしてあります。

reportBackToIOSは名前の通りiOSにリンクがタップされたことを知らせる役割を担っています。iOSのUIWebViewではWebView内でURLの読み込みが発生する直前にコールバックを通るんですが、ここではそれを利用しています。

具体的には、iOSに渡したい情報をURL(の一部)として持ったiframeを作って一旦HTMLのbodyに追加し、すぐに削除していますね。iframeがbodyに追加されたとき、srcアトリビュートで持っているURL(ここでは、‘callback://’ + 遷移先のURL)をiframe内で開こうとするため、WebViewのコールバックが呼ばれることになるわけです。

iOS側の対応

ViewControllerに適当にUIWebViewを置いてあり、ViewControllerViewControllerの遷移をするSegueがnextという名前で定義されているとします(実際にはViewControllerからViewControllerへのSegueは定義できないので、隠しボタンからViewControllerへのSegueです。)。

 1#define HOST @"http://localhost:4000"
 2
 3- (void)viewDidLoad {
 4    [super viewDidLoad];
 5
 6    self.webView.delegate = self;
 7    NSString* urlstring = NULL;
 8    NSString* path = self.segueOptions.stringValue;
 9    if(path) {
10        urlstring = [NSString stringWithFormat:@"%@%@", HOST, path];
11    } else {
12        urlstring = [NSString stringWithFormat:@"%@%@", HOST, @"/"];
13    }
14
15    NSURL* url = [NSURL URLWithString:urlstring];
16    NSURLRequest* urlRequest = [NSURLRequest requestWithURL:url];
17    [self.webView loadRequest:urlRequest];
18}

肝心のコールバックはこんな感じで実装しておきます。

1- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
2{
3    if ([[[request URL] scheme] isEqualToString:@"callback"]) {
4        NSString* href = [[[request URL] absoluteString] substringFromIndex:11];
5        [self performSegueWithIdentifier:@"next" options:href];
6        return NO;
7    }
8    return YES;
9}

shouldStartLoadWithRequestは新しいリクエストが来た時に通るコールバックで、何もしないときはYESを返すことで読み込みをスタートさせることができるわけですが、今回はcallback://で始まるURLの場合だけはnextという名前のSegueを実行してNOを返すことにします。NOを返せばURLの読み込みは行われないので、callback://****みたいなURLを処理しちゃってエラー、ということも起こりません。

これで最初に紹介したGIFアニメのようなページ遷移ができるようになりました。便利なような、使いどころ難しいようなそんな感じですが、ネイティブっぽく動かすという目的はなんとなく果たせていますね。

参考リンク

reportBackToIOSのところはStackOverflowのこの質問を参考にしました。

ios - How can I reliably detect a link click in UIWebView? - Stack Overflow