Post on 11-Sep-2014
description
iOS WebView App@hagino3000
2013-12-12
社内的な勉強会 at VOYAGE GROUP
注Androidの話は無いです
なぜWebView上にアプリを構築するのか
• 任意のタイミングでアプリケーションをアップデートしたい
• WebでUI作った方が楽そうだから
• そんな事は無い → あとで死ぬ
実装戦略
実装戦略
• HTML, CSS, JSのリソースをアプリにバンドルする or NOT
• Single Page Web App or NOT
• ネイティブUIパーツを使う or NOT
HTML, CSS, JSファイルをアプリにバンドルするか
• 完全にオフラインで動作させる事が可能
• それ以外メリット無し
Single Page Web Appにするか
• Single Page
• JSの実装量は増える
• なんらかのJSフレームワークを使うSencha Touch, AngularJS etc...
• Multi Page
• 作りはシンプルになる
(番外編) Multi UIWebView
• ページ毎にUIWebViewを作る
• ネイティブのトランジションが使える
• 試してない
iOSのUIパーツを使うか
iOSのUIパーツを使うか
• NavigationBar, UIAlertView, UIActionSheet etc..
• 使った方が楽 (モーダル制御等)
• ネイティブUIを呼び出すブリッジを作る
• ブラウザでデバッグできるようにPolyfillを用意しておく
UIWebView
UIWebView
iOS Version Browser Component
iOS 6 iOS Safari 6.x
iOS 7 iOS Safari 7.0
機能比較http://caniuse.com/#compare=ios_saf+6.0-6.1,ios_saf+7.0
コンテンツの配置
UIWebView
UIScrollView
HTML Content(表示領域)
Navigation Bar
Contents A
rea (iOS6)
Contents A
rea (iOS7)
document.scrollTop 不可視
不可視
ViewPort
<!-- よくある設定 --><meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0, maximum-scale=1.0"/>
スクロールの制御
• Single Page Web AppHeight 100%のブロック要素を並べて、その中でスクロールさせる場合はデフォルトのスクロールが邪魔になる。(二重スクロール)
• そうでない場合はscrollViewのスクロールを使う
スクロールの制御
// UIScrollViewのスクロールを無効にするwebView.scrollView.scrollEnabled = NO;
/* CSSでブロック要素の慣性スクロールを有効にする */article.page{ box-sizing: border-box; position: absolute; width: 100%; height: 100%; top 0; left: 0; overflow: scroll; -webkit-overflow-scrolling: touch;}
スクロール速度scrollView.decelerationRate = UIScrollViewDecelerationRateNormal;
// デフォルトは UIScrollViewDecelerationRateFast
慣性スクロールの減速が緩くなる。scrollViewのスクロールを使う場合は設定する。CSSでブロック要素の慣性スクロールを有効にした場合は UIScrollViewRateNormal 相当になる。
NavigationBar
• ポジション固定にする場合はネイティブのを使うと楽
• NavigationBarを半透明にして、背後にもコンテンツを配置したい (iOS7)
• 初期表示でnavbarの下にコンテンツが潜りこまないようにscrollViewの調節が必要
• webView.scrollView.contentInset
• webView.scrollView.contentOffset
• webView.scrollView.scrollIndicatorInsets
position: fixed
• 効かない• スクロールに引きずられる
• iScroll
• onScrollイベントを監視してposition:fixedを再現している
• https://github.com/cubiq/iscroll
• 代替策として別のViewにして配置するのもアリ
テキスト入力1. <input type=”text”> or <textarea> にフォーカス2. Keyboard Windowがせり上ってくる3. document.scrollTopがずれる
• テキスト入力パーツはなるべく画面上部に配置する。下の方だと最悪Keyboard Windowが被る。
• scrollViewのスクロールを無効にしているとユーザーが元に戻せない。→入力後にscrollTopを調整する。
• JavaScriptからキーボードの上げ下げを可能にしておく
その他よくやる設定<!-- 電話番号らしき数列をリンクにするのを無効化 --><meta name="format-detection" content="telephone=no">
body { /* 文字列のコピーや選択をできなくする */ -webkit-user-select:none;
/* リンクの長押しメニューを出なくする */ -webkit-touch-callout:none;
/* アンカー要素をタップした時に出現する枠を見えなくする */ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);}
ネイティブとの連係
To WebView
NSString *js = @”window.App.hello();”;[self.webView stringByEvaluationgJavaScriptFromString:js];
// 例:ネイティブからsessionId, userId etc.. を渡すNSString *js = [NSString stringWithFormat: @”window.App.deviceReady({\ sessionId: ‘%@’,\ userId: ‘%@’,\ apiBase: ‘%@’\ });”, self.sessionId, self.user_id, @”http://api.hoge.com”];
[self.webView stringByEvaluatingJavaScriptFromString:js];
From WebView
1. 独自スキーマを指定してlocationを変更2. UIWebViewDelegateのshouldStartLoadWithRequestでフック
3. なにかやる4. UIWebViewのstringByEvaluatingJavaScriptFromStringで結果を返す
ヒント:PhoneGapのソースの方がこのスライドよりも参考になるhttps://github.com/phonegap/phonegap/blob/master/lib/ios/CordovaLib/cordova.js
// JS側の実装例var transactionId = 0;
function nativeBridge(action, params, callback) { // 後で呼び出すためにコールバック関数を保持する var transactionId = transactionId++; callbacks[transactionId] = callback; var data = encodeURIComponent(JSON.stringify({ transactionId: transactionId, params: params })); // iframeのlocation変更リクエストはWebViewDelegateで // フックできる var frame = $('<iframe>').attr('src', 'MyActionHandler://' + action + '/' + data ).css({display: "none"}); $(document.body).append(frame); frame.remove();}
// Objective-C- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
// Check schema if ([[request.URL.scheme lowercaseString] isEqualToString:@”MyActionHandler”]) { // なんかやる return NO; } return YES;}
App.nativeBridge(‘alert’, { title: “Error”, message:“Sorry...”, buttonTitle: “OK”}, function(){ // UIAlertViewのOKボタンがタップされた})
使用例
最初から作っておくと便利
• キーボードWindowの上げ下げ• Alert• Confirm• ActionSheet• ジェスチャー認識
リクエストのフック
• UIWebViewDelegateのshouldStartLoadWithRequest
◯ ロケーション変更 (iframe含む)
◯ リロード (iframe含む)
◯ フォームサブミット× XMLHttpRequest× script要素、link要素、img要素による通信
• NSURLProtocol• 全ての通信をフックできる、ホワイトリストを実装するならこちら
リクエストのフックの応用
• ロケーション変更時、ドメインによって処理を変える(ex 自サービスの外に出る場合はSafariで開く)
• 認証情報の付加• NSMutableURLRequestを操作してcookieや独自ヘッダを付加する
• ホワイトリスト (NSURLProtocol)
• しかし、アドネットワークを利用して広告を表示しているとホワイトリストのメンテは困難を極める
パフォーマンス
Clickイベントが遅延する問題• タッチデバイスブラウザはtouchstart後clickイベントまでの間隔が300msec程度空く
• リンククリックの反応が遅い• もっさり感
•解決策• FastClick.js
• clickイベントが速くなる、リンククリックも改善• jQuery mobileであればvclickイベントを使う• clickでは無くtouchendイベントを使う
(ブラウザのデバッグが面倒になる)
DOM操作のチューニング
• DOM操作は最小限に、変更が必要な箇所のみ• アニメーションはGPUを効かせる
• -webkit-transform: translate3d を使う• レイアウトの再計算が発生する回数を減らす
• 例• offsetHeightを取得して挿入した要素の高さだけスクロール位置を下にずらす処理
• DOM操作全てが終ったタイミングで行なう
無限リスト
• DOMツリーが増え続けると重くなる• iOSのテーブルセルの再利用のような仕組みが必要になる• iOS SDKのUITabelViewCellの標準機能だけどDOMにはそんな物無いです……
デバッグ
ブラウザでデバッグ可能にする
Chromeで開発 and デバッグ↓iPhoneシミュレーターで開発 and デバッグ↓実機 (最終確認)
• ブラウザでデバッグする際はネイティブ機能呼び出しのブリッジを差しかえる (Alert, ActionSheet ...)
• AppDelegate相当の物を作っておく• イベント受信部• Push通知, Navigationbar button操作, on/offline..
• どの画面からでも開始できるようにしておく• 画面遷移して到達しないと機能しない画面はデバッグしづらい。なるべくステートレスに。
• Chromeのエミュレーションモード• iPhone5, 4S etc... 選べるがイマイチ
ブラウザでデバッグ
Chromeのエミュレートモード
• オートリロード必須• リロードするのが手間• grunt auto-reload を使う• JS, HTMLはソース編集と同時にリロード• CSSはDOMの状態を維持したまま変更が反映される
• window.onerrorでキャッチした例外をXcodeのログに吐くようにしておく(特に発生行数)
• Safariの開発メニューから、開発者コンソールをアタッチできる
iOSシミュレーターでデバッグ
• ローカルサーバーと通信してると何もかもが一瞬で取得できてしまう
• Network Link Conditioner を使う
• 使わないと3G回線の速度を再現できない• 通信中インジケーターをゆっくり眺められる
通信状況のエミュレート
通信状況のエミュレート
• 特にアニメーション、タッチレスポンス周りを確認• 通信中の電話着信や、アプリがバックグラウンドに行っても大丈夫か
実機でデバッグ
サーバーAPIの実装
• 素直なRESTful APIにしておく• Request
• content-type: application/json• Response
• content-type: application/json
設計
モダンなJavaScriptライブラリはこれが前提になっていたりする(ex. backbone.js)。道を外れると一気に工数が跳ね上がる。
• APIサーバーはCross Domain XHRに対応しておく• HTML, JS, CSSはlocalhost (grunt server)で編集
• APIサーバーはAWS上の開発機を参照してAjaxで叩く
• デザイナー or フロント担当エンジニアはローカルに静的ファイルの開発環境のみ作れば良い
• 認証情報を独自ヘッダに持たせている場合
• OPTIONSのプリフライトリクエストが飛ぶのでAPIサーバーをCORSに対応させておく
開発時
まとめ
• とにかくブラウザでデバッグする• UIの実装はネイティブ同様時間がかかる (楽はできない)
• 覚悟が必要
まとめ