Google+ もご覧ください
ユーザーアイコン

実践!RubyMotion

第4回 RubyMotionでリファクタリング。MVCの分離にチャレンジ!

Satococoa

第4回 RubyMotionでリファクタリング。MVCの分離にチャレンジ!

前回は Qiita API から取得したデータを UI に表示するところまで実装しました。今回は今後の機能拡張に備えて、リファクタリングを行ってみようと思います。

本来はテストのある状態でリファクタリングするべきですが、全てコントローラ上にロジックを実装してきてしまったためこのままではテストを書こうにも難しい状態です。そこで今回は機能が少ない今のうちに MVC への分離を行って、テストを書ける状態にリファクタリングしたいと思います。

基本的な方針は以下のようになります。

  1. ディレクトリ階層を整理する
  2. Qiita API とやり取りするコードをモデルとして切り出す
  3. 投稿のタイトル、日付、投稿者を表示しているテーブルのセルをビューに切り出す
  4. 投稿の内容 (本文) を表示するコードを別のコントローラに分離する

では順番に進めていきましょう。

1. ディレクトリ階層を整理する

現状のディレクトリ階層は以下のようになっているはずです。

$ tree
.
├── Gemfile
├── Gemfile.lock
├── Rakefile
├── app
│   ├── app_delegate.rb
│   └── entries_controller.rb
├── resources
│   └── Default-568h@2x.png
└── spec
    └── main_spec.rb

3 directories, 7 files

app/ ディレクトリの下に appdelegate.rb と entriescontroller.rb がそのまま入っています。

ファイルが増えたときに混乱してしまわないよう、app ディレクトリをさらに models, views, controllers の 3 つのディレクトリに分け、entries_controller.rb を app/controllers ディレクトリの下に入れましょう。

$ tree
.
├── Gemfile
├── Gemfile.lock
├── Rakefile
├── app
│   ├── app_delegate.rb
│   ├── controllers
│   │   └── entries_controller.rb
│   ├── models
│   └── views
├── resources
│   └── Default-568h@2x.png
└── spec
    └── main_spec.rb

6 directories, 7 files

2. モデルを切り出す

Qiita::Item

現状では Qiita API から取得したデータを Hash にして使っていますが、Qiita::Item クラスを実装してより扱いやすくしましょう。

以下の内容で app/model/qiita/item.rb を作成します。

app/model/qiita/item.rb

module Qiita
  class Item
    # 必要な値のみ保持する
    attr_accessor :title, :username, :updated_at, :body

    def initialize(data)
      @title      = data['title']
      @username   = data['user']['url_name']
      @updated_at = data['updated_at_in_words']
      @body       = data['body']
    end
  end
end

Qiita::Client

今度は Qiita API にアクセスして投稿を取得し、それぞれの投稿を Qiita::Item クラスのオブジェクトとして生成するコードを実装します。

また、データを取得後に非同期で任意の処理を実行できるようにしたいので、ブロックを渡せるような仕様にします。

Qiita::Client クラスの .fetch_tagged_items(tag_name) クラスメソッドとして実装してみました。

app/models/qiita/client.rb

module Qiita
  class Client
    BASE_URL = 'https://qiita.com/api/v1'
    # Qiita API にアクセスして特定のタグがつけられた投稿を取得する
    # 
    # @example
    #   Qiita::Client.fetch_tagged_items('tag') {|items, error_message|
    #     # 任意の処理
    #   }
    #
    def self.fetch_tagged_items(tag_name, &block)
      url = BASE_URL + "/tags/#{tag_name}/items"
      BW::HTTP.get(url) do |response|
        items = []
        message = nil
        begin
          if response.ok?
            json = BW::JSON.parse(response.body.to_s)

            # 1 件ずつ Qiita::Item クラスのインスタンスにして格納
            items = json.map {|data| Qiita::Item.new(data) }
          else
            # エラーが起きた場合は message 変数にエラー内容を格納
            if response.body.nil?
              message = response.error_message
            else
              json = BW::JSON.parse(response.body.to_s)
              message = json['error']
            end
          end          
        rescue => e
          p e
          items = []
          message = 'Error'
        end
        block.call(items, message)
      end
    end
  end
end

まだリファクタリングできそうですが、一旦ここまでにとどめておきます。

ページネーションの処理などを考慮すると Qiita::Response のようなクラスを作った方が良いと思いますので、ぜひご自分で試してみてください。

コントローラを修正する

モデルへのコードの分離が終わったので、Qiita::ClientQiita::Item を使うように EntriesController を修正しましょう。

修正するメソッドは viewDidLoadtableView:cellForRowAtIndexPath:tableView:didSelectRowAtIndexPath: の 3 つです。

viewDidLoad

def viewDidLoad
  super

  @tag = 'RubyMotion'
  self.title = @tag
  @entries = []

  Qiita::Client.fetch_tagged_items(@tag) do |items, error_message|
    if error_message.nil?
      @entries = items
      self.tableView.reloadData
    else
      p error_message
    end
  end
end

tableView:cellForRowAtIndexPath:

def tableView(tableView, cellForRowAtIndexPath:indexPath)
  cell = tableView.dequeueReusableCellWithIdentifier(ENTRY_CELL_ID)

  if cell.nil?
    cell = UITableViewCell.alloc.initWithStyle(UITableViewCellStyleSubtitle, reuseIdentifier:ENTRY_CELL_ID)
  end

  entry = @entries[indexPath.row]
  cell.textLabel.text = entry.title
  cell.detailTextLabel.text = "#{entry.updated_at} by #{entry.username}"
  cell
end

tableView:didSelectRowAtIndexPath:

def tableView(tableView, didSelectRowAtIndexPath:indexPath)
  entry = @entries[indexPath.row]

  # UIWebView を貼付けた ビューコントローラを作成
  controller = UIViewController.new
  webview = UIWebView.new
  webview.frame = controller.view.frame # webview の表示サイズを調整
  controller.view.addSubview(webview)

  # 画面遷移
  navigationController.pushViewController(controller, animated:true)

  # HTML を読み込む
  webview.loadHTMLString(entry.body, baseURL:nil)
end

「URL を組み立て、HTTP の GET メソッドでアクセスし、成功したらレスポンスの JSON をパースして...」といった実装の詳細をコントローラからモデルへと移動させることが出来ました。

rake コマンドを実行して、修正前と同様に動作することを確認しておきましょう。

3. ビューを切り出す

次は投稿のタイトル・投稿者名・投稿日を表示しているテーブルのセルをビューに切り出してみましょう。

EntryCell

UITableViewCell を継承して EntryCell クラスを作ります。 app/views/entry_cell.rb を以下の内容で作成してください。

app/views/entry_cell.rb

class EntryCell < UITableViewCell
  attr_accessor :entry

  def initWithStyle(style, reuseIdentifier:reuseIdentifier)
    # UITableViewCellStyleSubtitle スタイルを使う
    super(UITableViewCellStyleSubtitle, reuseIdentifier:reuseIdentifier)
  end

  def drawRect(rect)
    super
    self.textLabel.text = @entry.title
    self.detailTextLabel.text = "#{@entry.updated_at} by #{@entry.username}"
  end
end

Qiita::Item クラスのオブジェクトを EntryCellentry 属性として渡し、ラベルにタイトルや名前を表示する処理はビューに任せてしまおうという設計です。

コントローラの修正

EntryCell クラスを利用するようにコントローラを少し修正しましょう。

この際、registerClass:forCellReuseIdentifier メソッドを使って Entry という reuseIdentifier に相当するクラスは EntryCell であることを宣言しておくことで tableView:didSelectRowAtIndexPath: をシンプルにできます。

viewDidLoad に以下の一行を追加してください。

self.tableView.registerClass(EntryCell, forCellReuseIdentifier:'Entry')

tableView:didSelectRowAtIndexPath: は以下のようになります。かなりすっきりしましたね。

def tableView(tableView, cellForRowAtIndexPath:indexPath)
  cell = tableView.dequeueReusableCellWithIdentifier(ENTRY_CELL_ID, forIndexPath:indexPath)

  entry = @entries[indexPath.row]
  cell.entry = entry
  cell.setNeedsDisplay # 再描画させる

  cell
end

このようにすることでコントローラの責務は「ビューに Qiita::Item クラスのオブジェクトを渡し、再描画してもらう」だけになりました。

rake コマンドを実行して、修正前と同様に動作することを確認しておきましょう。

4. コントローラの分離

最後に投稿の本文を表示させる処理を別のコントローラとして実装します。UIViewController を継承して EntryController を作りましょう。

先ほどの EntryCell と同様に Qiita::Item クラスのオブジェクトを渡し、あとは EntryController に全てを任せます。

app/controllers/entry_controller.rb

class EntryController < UIViewController
  attr_accessor :entry

  def viewDidLoad
    super

    webview = UIWebView.new
    webview.frame = view.frame # webview の表示サイズを調整
    view.addSubview(webview)

    webview.loadHTMLString(@entry.body, baseURL:nil)
  end
end

では EntriesControllertableView:didSelectRowAtIndexPath: メソッドを修正して、このコントローラを使うようにしてみましょう。

def tableView(tableView, didSelectRowAtIndexPath:indexPath)
  entry = @entries[indexPath.row]

  controller = EntryController.new
  controller.entry = entry

  # 画面遷移
  navigationController.pushViewController(controller, animated:true)
end

EntriesController がだいぶすっきりしたと思います。ここまでで一通りリファクタリング終了とします。

rake コマンドを実行して、動作することを確認してください。

まとめ

今回は前回作ったサンプルを MVC に分けるリファクタリングをしてみました。

コードばかりの回になってしまいましたが、RubyMotion を使った iOS アプリケーション開発の具体的なイメージを持っていただけたのではないかと思います。 直接 Cocoa Touch フレームワークを触ることもできますし、それをラップすることでより Ruby らしく書くこともできるのが RubyMotion の面白さです。

今回のサンプルコードは こちらからダウンロード できます。

次回以降はいよいよテストを書いてみたいと思います。

Rubymotion
タグ:

記事をリクエストする

関連記事

コメント