いものやま。

雑多な知識の寄せ集め

iOSでローカルのHTMLを表示する方法について。

変種オセロのルールを表示する方法として、ルールを書いたHTMLをローカルに用意して、それを表示するのがいいと思ったので、iOSでローカルに置かれたHTMLを表示する方法について調べてみた。

WKWebView

HTMLを表示させるならUIWebViewかなと思ったのだけど、Appleのドキュメントを見ると、UIWebViewを使うのではなくWebKitフレームワークのWKWebViewを使うことが推奨されていた。
なので、ここではWKWebViewを使っていくことにした。

WKWebViewでHTMLを表示するためのメソッドは、主に二つ。
一つは、WKWebView#loadHTMLString(_: String, baseURL: NSURL?)、もう一つは、WKWebView#loadRequest(_: NSURLRequest)
前者は引数にHTMLの文字列を直接指定して表示させるもので、後者はURLへのリクエストに対するレスポンスを表示させるものになっている。

ローカルのHTMLを表示させたい場合、基本的にはどちらを使ってもOK。
すなわち、ローカルに置かれたHTMLファイルの内容を読み込んで、それをloadHTMLStringの引数に指定してもいいし、ローカルのHTMLファイルのURLに対するリクエストを作って、それをloadRequestの引数に指定してもいい。

ただし!
何やらWKWebViewにバグがあるらしくて、現状ではアプリのテンポラリディレクトリ(/tmp)以下に置かれたファイルでないと、正しく読み込まれないという問題があるみたい。
実際試してみると、例えば画像を読み込む必要があるHTMLをloadHTMLStringで表示させてみたところ、画像が表示されなかったりした。
なので、このバグを回避してやる必要がある。(※2015年9月現在)

iOSでのファイル操作

ただ、そもそもの話として、iOSでのファイル操作ってどうすればいいんだ?というのがあったり。
例えば、「ローカルにHTMLファイルを置くには、XCodeでどうすればいいの?」とか「置いたHTMLファイルのURLはどうなるの?」とか。
あるいは、「どうやってファイルを読み書きすればいいの?」「ファイルを移動やコピー、削除するにはどうすればいいの?」「ディレクトリ構成はどうすべきなの?」とか。

まず、ローカルにHTMLファイルを置くには、単にXcodeでファイルを追加すればいい。
この追加する場所はどのディレクトリ、グループでもよくて、追加した場所に依らず、基本的にバンドル(/アプリ名.app/)以下にフラットに置かれるみたい。
(ドキュメントが見当たらないので、確信は持てないのだけど・・・)
ここで「基本的に」と書いたのは、ファイルのローカライズを行った場合、それぞれの言語用のリソースが置かれるディレクトリ(例えば、Base.lprogやja.lprog)以下にファイルが置かれることになるから。
なお、置いたファイルのURLを取得するときにはNSBundle#URLForResource(_: String?, withExtension: String?)を使えば、使われてる言語に応じたリソースのURLが取得できる。

次に、ファイルを置くことが出来て、そのURLも取得できたとして、そのファイルの内容を読み書きするにはどうしたらいいかという話。
一番簡単なのは、NSStringのイニシャライザNSString#init?(contentsOfURL: NSURL, encoding: UInt, error: NSErrorPointer)と、メソッドNSString#writeToURL(_: NSURL, atomically: Bool, encoding: UInt, error: NSErrorPointer)を使う方法。
文字列クラスによくこんなのを用意するなと思うけどね(^^;

そして、ファイルの移動やコピー、削除といった操作を行うには、NSFileManagerを使うといい。
かなり強力なメソッドまで用意されている感じ。
(例えば、ディレクトリを作るときに、中間のディレクトリもなければ作ってくれたりとか)

サンプルコード

ということで、サンプルコード。

プロジェクトのファイルは、以下のような感じ。

f:id:yamaimo0625:20150908161413p:plain

HTMLファイルが2つ(一方から他方へリンクが貼ってある)と、HTMLで表示する画像を用意してある感じ。
なお、それぞれローカライズしてある。

そして、ViewControllerは以下のとおり。

//
//  ViewController.swift
//  WebViewTest
//

import UIKit
import WebKit

class ViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    
    let webView = WKWebView(frame: self.view.frame)
    self.view.addSubview(webView)
    
    let urlopt = NSBundle.mainBundle().URLForResource("test", withExtension: "html")
    if let url = urlopt {
      println("url: \(url)")
      
      let directory = url.URLByDeletingLastPathComponent!
      println("directory: \(directory)")
      
      let fileManager = NSFileManager.defaultManager()
      let temporaryDirectoryPath = NSTemporaryDirectory()
      let temporaryDirectoryURL = NSURL(fileURLWithPath: temporaryDirectoryPath)!
      println("temporary directory: \(temporaryDirectoryURL)")
      
      if fileManager.createDirectoryAtURL(temporaryDirectoryURL,
                                          withIntermediateDirectories: true,
                                          attributes: nil,
                                          error: nil)
      {
        let targetURL = temporaryDirectoryURL.URLByAppendingPathComponent("www")
        let htmlURL = targetURL.URLByAppendingPathComponent("test.html")
        let htmlopt = NSString(contentsOfURL: htmlURL, encoding: NSUTF8StringEncoding, error: nil)
        if let html = htmlopt {
          let originalHTML = NSString(contentsOfURL: url, encoding: NSUTF8StringEncoding, error: nil)
          if html != originalHTML {
            println("delete and copy.")
            fileManager.removeItemAtURL(targetURL, error: nil)
            fileManager.copyItemAtURL(directory, toURL: targetURL, error: nil)
          }
        } else {
          println("copy.")
          fileManager.copyItemAtURL(directory, toURL: targetURL, error: nil)
        }
        
        let request = NSURLRequest(URL: htmlURL)
        webView.loadRequest(request)
      } else {
        println("failed to create directory.")
      }
    } else {
      println("url is nil.")
    }
  }

  override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
  }

  override func prefersStatusBarHidden() -> Bool {
    return true
  }
}

いろいろ調べるためのコードがちょいちょい残ってるけど、やっているのは以下のとおり。

  1. WKWebViewを作って、サブビューとして追加。
  2. メインバンドルからローカルのHTMLファイルのURLを取得。
  3. ローカルのHTMLファイルの置かれているディレクトリを取得。
  4. テンポラリディレクトリのURLを取得。
  5. (WKWebViewのバグを回避するために)コピー先のディレクトリとコピー先のファイルのURLを取得。
    1. もしコピー先のファイルが存在していたら、その内容を読み取って、コピー元の内容と比較。
      もしコピー元とコピー先の内容が異なっていたら、コピー先のディレクトリを削除してから、コピー元のディレクトリをコピーする。
    2. そうでなければ、単にコピー元のディレクトリをコピーする。
  6. リクエストを作成し、WKWebViewに表示させる。

毎回コピーするのも微妙なので、コピー先のファイルが存在していなかったり、あるいはコピー元とコピー先でファイルの内容が異なっていた場合にだけコピーするようにしている。
なお、テンポラリディレクトリの内容はアプリが動作していないときに削除されることがあるようなので、そのこともちょっと気をつけておかないといけない。

あと、今回は手抜きでディレクトリを丸ごとコピーしたけど、ホントはちゃんとファイルのリストを作ってコピーした方がいい。
じゃないと、余計なファイルまでコピーして、時間をくうことになるので。

実行すると、次のような感じ。

f:id:yamaimo0625:20150908163925p:plain

画像もちゃんと表示されるし、リンクをクリックすればちゃんとリンク先のHTMLも表示される。

そして、言語設定を日本語に変更して再度実行すると、次のような感じ。

f:id:yamaimo0625:20150908164101p:plain

このように、日本語にローカライズした方のHTMLがちゃんと表示される。

今日はここまで!