武之新
[iPhone]サイトをローカル保存するための格闘

iPhoneを使っていて、最もイライラするのは通信状況。電波の受信感度はバッチリでも通信できないということが珍しくありません。
iPhoneのせい、というよりは、Softbankの電波のせいなわけですが。

iPhoneのSafariは(というか普通のブラウザはみな)一回みたページに戻る場合でも 最新かどうかを判断するため通信しようとするので、通信状況が不安定だとブラウジングがものすごくイライラします。

そこで一度みたページをすべてキャッシュし、キャッシュがあれば最新チェックもなにもかもすっ飛ばして表示してくれるアプリを作ろうとしています。が、これがなかなかに難儀しています。

いままでチャレンジしてみたこととしては、

(1) webView:shouldStartLoadWithRequest:navigationType: で保存すべきURLがわかるか?

  1. ドキュメントのURLはこれでわかるけど、画像やCSSなどのリソースについては呼び出されないのでわからない。さらにJavascriptでXHRとかしてるとそれは呼び出されてしまい、なにがページでなにがXHRかわからない。

(2) いっそのこと webView:shouldStartLoadWithRequest:navigationType: に飛んで来たURLからHTMLを取得し、中身をparseして画像やCSSなどのリンクをたどればできるんじゃないか?

  1. HTML、CSSのparseはできた。
  2. GoogleやYahooなどではCSSをJavascriptで後から追加するので、HTMLだけparseしても正しく表示できない。
  3. Javascriptのparseはさすがにちょっと無理。

(3) uiWebView:resource:willSendRequest:redirectResponse:fromDataSource: でなら画像やCSSも含めて呼び出されているっぽいからいけるんじゃない?

  1. 概ねこれで保存対象がわかるようではあるが、一部のリソースは呼ばれない模様。NSURLCacheのcachedResponse:forRequest: をチェックしているとwillSendRequestに飛んで来てないURLがcachedResponse:forRequest: されていたりしている。
  2. キャッシュしているリソースであればrequestのURLをfile://に書き換えたいが、それをすると落ちる。特にGoogleのトップページとか。
  3. 2が回避不能なのでこれだけでは解決できず。

(4) NSURLCacheでどうよ?

  1. キャッシュを返しても落ちない!キャッシュを返すのはこれでイケる。
  2. UIWebViewWebViewDelegateよりも呼ばれる数が多いので、こちらが呼ばれたものが本当に通信しにいっているリソースだと思われる。なので、これで保存対象がわかる。 
  3. storeCachedResponse:forRequest: はredirectされていても呼ばれてしまう模様。NSCachedURLResponseだけではredirectされたものかどうかの判断ができそうにないので、保存は無理。

という流れ。
結論としては、(4)でなんとかなりそう。
ただしstoreCachedResponse:forRequest: はredirectの絡みで使えないため、

  • cachedResponse:forRequest: で保存対象を特定
  • 自力で保存
  • 自力保存したリソースが要求されたらそれをcachedResponse:forRequest: で返す

という実装になりそうである。なかなか大変。