Edited at

MyAnimeList を日本で使うために必要なたったひとつの冴えたやりかた

More than 1 year has passed since last update.

この記事は 「DeNA IPプラットフォーム事業部 Advent Calendar 2017」

20日目の小ネタ記事です。

(昨日の ぬにゅ がマジカワです)

20日目の小ネタは WKWebView を使って、 Web サイトを自分好みにカスタマイズした iOS アプリを作ってみましたーという内容です。

(iOS エンジニアの方には 3周遅れぐらいの内容ですみません)


はじめに(読み飛ばし可)

MyAnimeList という海外ユーザ向けのアニメ視聴記録 & アニメ特化 Wikipedia みたいなサービスを開発している @rinunu です。

自分の作っているサービスをドッグフーディングしたいんです。


でもそこには壁がありました。

dアニメストア」という壁が。

そう、日本でアニメを見るなら「月額400円(税別)でアニメ見放題」な dアニメストアです。


今期の「魔法使いの嫁」から昔の 「Fate/stay night」まで、いろんなアニメが見放題。タイトル数もとても豊富です。

でも MyAnimeList は海外向けのサービスなので CrunchyrollHulu とのみ提携しており、 dアニメストアとは連携していないんです。

MyAnimeList に「観たい」登録したアニメを dアニメストアで観たい。

そんな日本の MyAnimeList ユーザ(推定13人)のためのソリューションを考えてみました。


ターゲットユーザのペルソナ(読み飛ばし可)

氏名: 土橋 りん(どばし りん)

年齢: 28歳

居住: 三軒茶屋から徒歩15分のアパート

職業: プログラマー

肩書: なし

家族構成: 未婚

IT 企業の社員である土橋りんは、平日は職場で馬車馬のように働いている。

独身であり、社内に好きな人はいるが、恥ずかしいので自分からは声をかけられない。

そんなりんの唯一の楽しみは、家のクイーンベッドを一人で専有してゴロゴロしながら、愛用の iPhone 6 Plus を使って「月額400円(税別)でアニメ見放題」の dアニメストアでアニメを見ること。

外国かぶれなので、観たいアニメと観たアニメは、アニメ視聴記録サイトである MyAnimeList を使って記録している。

「観たい」アニメがあると MyAnimeList に登録し、 dアニメストアでアニメを見た後、 MyAnimeList で「観た」と記録するというのがもっぱらの使い方。

視聴の際は AirPlay を使って Apple TV に映しつつ、ゴロゴロしている。

視聴時、部屋の電気は消して、アニメに没頭できるようにしている。

将来に漠然とした不安を抱えているが、 400円で好きなだけアニメを見ることができる dアニメストアがあるので、独居老人になってもなんとかなるかなとポジティブシンキング。


考察

ターゲットユーザの特徴から、 MyAnimeList に以下のような機能があると望ましいと考えられます。


  • iPhone の Safari で開いている MyAnimeList の "観たい" アニメ一覧やアニメ詳細ページから、 dアニメストアのアプリにジャンプできること。


イメージ

Untitled.png


実装方針検討


  1. iPhone の Safari で開いた MyAnimeList ページに、 dアニメストアへのリンクを追加する拡張を作りたい。

    ただ、調べてみると App ExtensionAction くらいしか使えそうなものがない。

    これだとユーザが明示的にアクションを実行しないとリンクが表示されないので、望みのものが作れない。

  2. iPhone の Chrome は拡張機能に対応していない。


  3. Greasemonkey みたいなものが使えるブラウザ を使う。

    なくはないけど、もう少しプログラミングしたい気持ち。

  4. proxy サーバを作って、そこでページを書き換える。

    良さそうだけど、せっかくなのでやったことないことをしたい。

  5. いっそ MyAnimeList 専用ブラウザを作る。

    このブラウザで MyAnimeList を表示した際に、カスタムの JS を実行して、 dアニメストアへのリンクを追加する。

    iOS アプリちゃんと作ったことないから楽しそう!

というわけで、 5 でいくことにしました。


構成

構成



  1. Cloud Firestore に「MyAnimeList と dアニメストアの紐付け情報」を入れておきます。


  2. WKWebView を使用して MyAnimeList のページを表示します

  3. 表示した MyAnimeList のページにカスタムの JS と CSS を埋め込みます。

  4. カスタムの JS にて Cloud Firestore から「MyAnimeList と dアニメストアの紐付け情報」を取得し、アイコンを表示します。

今回はサーバを持ちたくなかった & 使ってみたかったので Cloud Firestore を使用してみました。


Cloud Firestore は Google の提供するサービスです。

クラウド上にデータを保持し、その最新のデータにクライアントから簡単にアクセスすることができます。

また、 iOS アプリを作ったことがなかった & 最終的には SPA にしたいという目論見もあったので、処理はなるべく JS 側で行うようにしました。


実装内容


ソース

https://github.com/rinunu/mal-client


ステップ 1: Cloud Firestore に「MyAnimeList と dアニメストアの紐付け情報」を入れる

紐付け情報を作って Cloud Firestore に入れます。

(詳しくは https://firebase.google.com/docs/firestore/quickstart を御覧ください。 Web UI からデータを投入することもできます。)

dアニメストアには約2,300作品あるので、1作品 1分で紐付けたとすると、38時間くらいで終わりました。

(MyAnimeList のアニメと dアニメストアのアニメは 1 対 1 で紐付かないので要注意)


ステップ 2, 3: WKWebView の操作

WKWebView というのは iOS のいわゆる普通の WebView です。

ここでやりたいのは MyAnimeList のページを表示、 カスタムの JS と CSS を埋め込むという処理です。

以下では WKWebView の使い方を説明します。


WKWebView を作る

Storyboard では作れないので、コードで作ります。

コンテナになる UIView を作って、そこに入れるのが楽でした。

webView = WKWebView()

webView.translatesAutoresizingMaskIntoConstraints = false
mainView.addSubview(webView)
webView.topAnchor.constraint(equalTo: mainView.topAnchor, constant: 0.0).isActive = true
webView.bottomAnchor.constraint(equalTo: mainView.bottomAnchor, constant: 0.0).isActive = true
webView.leadingAnchor.constraint(equalTo: mainView.leadingAnchor, constant: 0.0).isActive = true
webView.trailingAnchor.constraint(equalTo: mainView.trailingAnchor, constant: 0.0).isActive = true


ページを表示

let request = URLRequest(url: url)

webView.load(request)


スワイプで戻ったりできるようにする

webView.allowsBackForwardNavigationGestures = true


JS を実行する

任意の JS を実行することができます。

webView.evaluateJavaScript("console.log('test');")

このメソッドは結果をコールバックで受け取ることもできます。


JS を埋め込む

webView に読み込んだページにカスタム JS を埋め込むことができます。

let jsSource = try! String(contentsOfFile: path)

let userContentController = WKUserContentController()
let userScript1 = WKUserScript(source: jsSource, injectionTime: WKUserScriptInjectionTime.atDocumentEnd, forMainFrameOnly: true)
userContentController.addUserScript(userScript1)

let configuration = WKWebViewConfiguration()
configuration.userContentController = userContentController

// WKWebView 作成時に configuration として渡す
webView = WKWebView(frame: .zero, configuration: configuration)

埋め込んだ JS の実行タイミングは WKUserScriptInjectionTime で制御します。

この方法で JS を実行すると、次に説明するように、 JS 側から native 側にメッセージを送ることもできます。  


JS から native へメッセージを送る

今回のアプリでは使用していませんが、 JS から native にメッセージを送ることができます。


native 側

self は WKScriptMessageHandler を実装しておく必要があります。

これは、 JS からのメッセージを受け取るものです。

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {

if (message.name == "myCallbackHandler") {
print(message.body as! String)
}
}

前述の WKUserContentController に WKScriptMessageHandler を登録します。

userContentController.add(self, name: "myCallbackHandler")


js 側

webkit.messageHandlers.myCallbackHandler.postMessage(myJson);


CSS を埋め込む

前述の JS の実行で埋め込みます。例えば

let cssString = try! String(contentsOfFile: cssPath)

let jsString = "var style = document.createElement('style'); style.innerHTML = `\(cssString)`; document.head.appendChild(style);"
webView.evaluateJavaScript(jsString)


WKWebView 内のブラウザをデバッグする

WKWebView 内のブラウザの状態をデバッグすることができます。

Safari の Develop メニューから Web Inspector を実行します。

これを使用すると、 JS や CSS のデバッグができます。


ページ読み込みのプログレスを表示する

Key-Value Observing(KVO) というものを使用して isLoading, estimatedProgress の変化を監視します。

このあたりの書き方は Swift のバージョンによってだいぶ異なるようです

(解説ページを色々みていて薄々そんな気はしていましたが、 Swift は変化が激速です)。

この例では block-based KVO (ドキュメント見つからず。。) というものを使用し、監視対象は Smart KeyPaths? を使用して指定するようにしてみました。

observations.append(webView.observe(\.loading) { _, _ in

if self.webView.isLoading {
self.progressView.isHidden = false
} else {
self.progressView.isHidden = true
self.progressView.setProgress(0.0, animated: false)
}
})
observations.append(webView.observe(\.estimatedProgress) { _, _ in
self.progressView.setProgress(Float(self.webView.estimatedProgress), animated: true)
})


独自の URL scheme を定義して、ユーザがリンクを実行した際に native 側で処理する

今回のアプリでは使用していませんが、以下のメソッドを使用すると、独自の URL scheme handler を定義できます。

(検証していません。)

setURLSchemeHandler(_:forURLScheme:)


通信内容の置き換え

今回のアプリでは使用していませんが、おそらく以下のようにすると通信内容を置き換えられるのではないかと思います。

(検証していません。)


  1. WKNavigationDelegatewebView(_:decidePolicyFor:decisionHandler:) を実装し、置き換えたい通信をキャンセルする。


  2. 自前で置き換え後の内容を作成し、loadHTMLString(_:baseURL:) する。



ステップ 4: カスタムの JS にて Cloud Firestore から「MyAnimeList と dアニメストアの紐付け情報」を取得し、アイコンを表示

このアプリでは JS で Cloud Firestore にアクセスするようにしました。

詳しくは https://firebase.google.com/docs/firestore/quickstart を御覧ください。


おわりに

WKWebView の説明がメインになってしまいましたが、 MyAnimeList に dアニメストアのリンクを追加して、日本人でも MyAnimeList を楽しめるよという方法を紹介しました。

アニメ好きな方は、ぜひ一度お試しください。

(MyAnimeList をドッグフーディングしない日本人なら、 Annict がおすすめです。)

明日は @daiking さんが、いろいろ振り返りますよー