前回(「iPhone アプリらしく #1」)、画面のデザイン(おもに意匠)を示してから、途中寄り道をしていたりもしたが、ようやく iPhone アプリらしい画面が動くようになった(一部、未実装の機能あり)。
Blogger Glass は(今のところ) Ajax はもちろん、JavaScript すら使っていない、純粋に画面遷移だけによるとても古臭いウェブアプリだが、それでも画面を整えてやることでグッと iPhone アプリらしく見えてくる。
実装方針
デバイスによって画面テンプレートとスタイルの切り替える
iPhone アプリらしい画面を Mac 用のものと共通にしようとするのは骨が折れるばかりだとわかった。同じ iOS デバイスの iPad にしたところで、画面の大きさがここまで違うと同じ画面を使い回すのはユーザ体験を低下させかねない。iPad のアプリの多くが「HD」と銘打って(iPhone と兼用ではなく) iPad 専用となっているのもうなずける。思い切って、画面を作るためのテンプレートとスタイルシートは iPhone 専用のものを作ることにした。
Blogger Glass を作り始めたときから、画面の構造(テンプレート)は画面ごとに用意していた。画面が増えてきたときに共通部分をくくり出すことにしたが、取り出せたのは、head
要素の他は画面の上部と下部に表示させることにした一部の情報用の領域のみ(図の App Header と App Footer)。
iPhone 用の画面を作っているときに気付いたが、この共通情報は、ブログなどのウェブサイトの構造を引きずったものだ。ウェブアプリには向かない、むしろ不要と言って良いものだった。そんなわけで、iPhone 用の画面構造(テンプレート)では、App (Header|Footer) をばっさり削った。以下に示す通り、iPhone 用にも base.html は存在するものの、共通化されているのはほぼ head
のみになっている。
(src/templates の構造) templates/ +-- base.html +-- iphone/ | +-- base.html | +-- list.html | +-- menu.html | +-- post.html | +-- search.html | +-- settings.html +-- list.html +-- menu.html +-- post.html +-- search.html +-- settings.html
画面の構造が変われば、スタイルシートも変わる。id
や class
を駆使し、複雑なセレクタ指定を使えば、すべての画面に対して 1 つのスタイルシートファイルで対応することもできる。が、それは巨大なファイルになるし、複雑さを軽減させようとテンプレートを分けたのにスタイルシートで複雑さを増やしていたのでは片手落ちと言うものだろう。だから、スタイルシートも画面ごとに分けることにした。
ただし、共通部分の多い画面(一覧画面と検索結果画面等)ではスタイルシートファイルを @import
することで、同じスタイルの定義が複数の場所に散らばることの避けた。もっとも、こういう方針を厳守しているかと問われると、実は心もとなかったりする。CSS って書き方(文法)に自由度がありすぎて、統一した様式で書くのって難しいんだよ。
(src/stylesheets の構造) stylesheets/ +-- default.css +-- ipad.css +-- iphone/ | +-- common.css | +-- list.css | +-- menu.css (iphone/list.css を import; 一部独自スタイルで上書き) | +-- post.css | +-- search.css (iphone/list.css を import) | +-- settings.css +-- mac.css (default.css を import)
実装
デバイスに応じて画面の構造(テンプレート)とスタイルを切り替える仕組みが、新たに追加した device
モジュールだ。
device モジュール
このモジュールでは、クライアントに想定してる各種デバイスを表すクラスといくつかのユーティリティ関数を定義している。
リクエストハンドラからは、ユーティリティの 1 つ、ファクトリ関数を呼び出してリクエストデバイスを表現するオブジェクトを取得する。ファクトリ関数を以下に示す。
(src/device.py より) def get_device(user_agent, view): if user_agent.find('iPad') > -1: dev = iPad(view) elif user_agent.find('iPhone') > -1 or user_agent.find('iPod') > -1: dev = iPhone(view) else: dev = Mac(view) return dev
これを呼び出すリクエストハンドラのコードは以下のようになる。
(src/main.py より) user_agent = self.request.headers['User-Agent'] view = self.viewinfo.type self.viewinfo.device = device.get_device(user_agent, view)
ViewInfo
オブジェクトに格納された device
オブジェクトは、画面を定義しているテンプレートの切り替えと、画面テンプレートの中でスタイルを定義している CSS ファイルの参照に使用される。
(src/util.py より) def render_template(handler): view = handler.viewinfo.type path = handler.viewinfo.device.template return template.render(path, { 'app': handler.appinfo, 'view': handler.viewinfo, })
(src/templates/iphone/base.html より)
<!DOCTYPE HTML>
<html lang='{{ app.lang }}'>
<head>
[...snip...]
{% for css in view.device.stylesheets %}
<link type='text/css' rel='stylesheet' href="{{ css }}">
{% endfor %}
</head>
各種デバイスを表すクラスを示す。
(src/device.py より) class Device(object): def __init__(self, device, view): self.is_ios_device = False self.stylesheets = None self.template = None self.template = get_template(device, view) self.stylesheets = get_stylesheets(device, view) class iPhone(Device): def __init__(self, view): Device.__init__(self, 'iphone', view) self.is_ios_device = True
is_ios_device
属性は、専用テンプレートを使っている iPhone では不要だ。他のデバイス(Mac) とテンプレートを共有している iPad でまだ使っているため残してある。実際に、テンプレートとスタイルシートの切り替えを実現しているのは、基底クラス(device.Device
) の初期化中に呼び出されている以下の 2 つの関数だ。
(src/device.py より) def get_template(device, view): if device == 'iphone': name = ("templates/%s/%s.html" % (device, view)) else: name = ("templates/%s.html" % view) return os.path.join(os.path.dirname(__file__), name) def get_stylesheets(device, view): style_dir = 'stylesheets' if device == 'iphone': common_style = ("/%s/%s/common.css" % (style_dir, device)) view_style = ("/%s/%s/%s.css" % (style_dir, device, view)) return [common_style, view_style] else: return [("/%s/%s.css" % (style_dir, device))]
これを見ればわかるように、切り替えといっても大袈裟なものではなく、デバイス名と画面のタイプをそのままディレクトリの構造とファイル名にしているだけ。
この部分を実装し始めた当初は、テンプレートもスタイルシートもパス名を自由に設定できるような作りにしていた。一言でいえば、デバイス名と画面タイプをキーとする二重のディクショナリだ。で、その二重ディクショナリにセットする値(つまりファイル名)を書いていて、何かひどく間違ったことをやっている気になった。
確かに、ひとつひとつファイル名を設定できた方が自由度が上がる。極端な場合を想像すれば、アプリ外部の URL を(テンプレートはともかくスタイルシートなら)使うことだってできる。しかし、それは過剰な(そして何より不要な)自由度ではないだろうか? そう思えてきた。
このとき頭の中にひとつの言葉が浮かんできた。それは "Convention over Configuration"、そう Rails の基本理念の 1 つだ(→ 「誰が Redmine を起動したのか?」の「Convention over Configuration (設定よりも規約)」を参照)。
デバイス名と画面タイプ(の名前)でファイルが特定できるなら、それをそのままファイルシステムにマップすれば良い。iPhone 用の一覧(list)画面なら iphone/list.html と決めれば良い。誰でもすぐに思い付く自然な規約(convention)だろう。むしろ直感的と言って良いほど。
(プログラマにとって)大事なことを思い出せたおかげで、コードはグッとシンプルなものになった。中途半端に(デバイス名による)条件分岐が残っているのは、今回採用したテンプレートとスタイルシートを画面ごとに分割する方式に則っているのが iPhone の場合だけだから。ま、そのうち、直すとしよう(iPad 用の画面を作るときかな)
jQTouch
今回の更新から、「iPhoneアプリケーション開発ガイド」で紹介されている jQTouch というライブラリを使っている。といっても、使っているのは jQTouch の配布にふくまれているボタン等の背景画像のみだ(iPhone 用の画面で使用)。
やはり外部で配布されているものなので、画像を src/images ディレクトリにコピーするような乱暴なことはやらず、配布物の構造そのままに src/lib に置くことにした (GData ライブラリと同様に bgithub のリポジトリには入れていない)。このため、iPhone 用のスタイルシートからは /lib/jqtouch/themes/jqt/img/button.png のような URL でアクセスしている。
スクリーンショット
この iPhone 対応版はすでに appspot に配備ずみ。ボタンの機能等で未実装部分が一部、残っているが、主要機能は iPhone/iPod touch で動かすことができる。以下は、実際に GAE アプリとして動かした様子を iPod touch (Retina ディスプレイ搭載) で撮ったスクリーンショットだ。
今後の展開
もちろん、未実装の部分(機能しないボタンとか)を実装しなければならない。「Back」ボタンなどは簡単に見えて、実装するには戻り先となる前の画面の情報を覚えておかなければならないため、それほど単純なものではない。GAE のデータストアサービスを使うか、あるいは(まだ使い方を調べていないけど) memcache サービスの方が良いのか。「Labels」(ラベル一覧画面を開く)も、ラベルの一覧をどこに保持しておくかで同様の課題がある。ま、見かけほど簡単じゃないと思ったから後回しにしているわけだけどね。
実用的になってくると毎日使うことになり、使えば使うほど「あら」も「ぼろ」も見えてくる。見えれば、「あら」は埋めたくなるし、「ぼろ」は繕いたくなる。やることはまだまだあるってことだ。
iPhone に専用画面で対応したんだから iPad にも同じようにしたい。記事の中身を読むという点では iPad の方が読みやすいんだから。
あと、iPhone 用としてはオフラインで記事を読めるようにしたい。ネットにつながらなかったり、つながっても遅かったりすると、やはりオフライン機能が欲しくなる。HTML5 のオフラインアプリケーションキャッシュ(「iPhoneアプリケーション開発ガイド」6 章で解説されている)を使えば可能かもしれない。しかし、そこまで考えるなら、もういっそのこと Objective-C で書き直すべきかもしれない。GData クライアントは Objective-C 版 (gdata-objectivec-client) もあるしな。
参考文献
関連リンク
- JavaScript (Wikipedia:ja)
- Ajax (Wikipedia:ja)
- jQTouch - jQuery plugin for mobile web developoment (公式サイト)
- Blogger Glass (GAE アプリ)
- mnbi/bloggerglass (github)
0 件のコメント:
コメントを投稿