2010-10-30

iPhone アプリらしく #2 - 実装 (Blogger Glass)

Blogger Glass: List view

前回(「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

画面の構造が変われば、スタイルシートも変わる。idclass を駆使し、複雑なセレクタ指定を使えば、すべての画面に対して 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 ディスプレイ搭載) で撮ったスクリーンショットだ。

iPod touch によるスクリーンショット
一覧画面
画面上部左にはメニュー画面を開くためのボタンを配置。 画面下部は一覧のページ切り替えのためのボタンを配置。 また、ページ切り替えのボタンはページに応じて有効、無効が変化する。 無効の場合は、ボタンの文字がグレーで表示される。
メニュー画面
画面上部左にはメニューを開く前の画面に戻るたけのボタンを配置(機能は未実装)。 メニューの項目はログインの状態によって変化する。
設定画面
画面上部左にはメニューを開く前の画面に戻るたけのボタンを配置(機能は未実装)。 「Save Settings」等、HTML フォームのボタンについてはスタイル(文字の大きさ等)を検討中。
記事画面
画面上部左にはメニューを開く前の画面に戻るたけのボタンを配置(機能は未実装)。 画面下部左にはラベル一覧画面を開くボタンを配置(機能は未実装)。
検索結果画面
画面上部左にはメニューを開く前の画面に戻るたけのボタンを配置(機能は未実装)。 その他は一覧画面と同様(下部のページ切り替えボタン等)。

今後の展開

もちろん、未実装の部分(機能しないボタンとか)を実装しなければならない。「Back」ボタンなどは簡単に見えて、実装するには戻り先となる前の画面の情報を覚えておかなければならないため、それほど単純なものではない。GAE のデータストアサービスを使うか、あるいは(まだ使い方を調べていないけど) memcache サービスの方が良いのか。「Labels」(ラベル一覧画面を開く)も、ラベルの一覧をどこに保持しておくかで同様の課題がある。ま、見かけほど簡単じゃないと思ったから後回しにしているわけだけどね。

実用的になってくると毎日使うことになり、使えば使うほど「あら」も「ぼろ」も見えてくる。見えれば、「あら」は埋めたくなるし、「ぼろ」は繕いたくなる。やることはまだまだあるってことだ。

iPhone に専用画面で対応したんだから iPad にも同じようにしたい。記事の中身を読むという点では iPad の方が読みやすいんだから。

あと、iPhone 用としてはオフラインで記事を読めるようにしたい。ネットにつながらなかったり、つながっても遅かったりすると、やはりオフライン機能が欲しくなる。HTML5 のオフラインアプリケーションキャッシュ(「iPhoneアプリケーション開発ガイド」6 章で解説されている)を使えば可能かもしれない。しかし、そこまで考えるなら、もういっそのこと Objective-C で書き直すべきかもしれない。GData クライアントは Objective-C 版 (gdata-objectivec-client) もあるしな。

参考文献

関連リンク

関連記事

0 件のコメント:

コメントを投稿