読者です 読者をやめる 読者になる 読者になる

Swift と Rails で iOS アプリ開発

Swift iOS Rails

フロントエンドを Swift で、バックエンドを Rails でアプリを開発してみます。

Web API 側実装

grape という gem を使って Web API を作っていきます。
以下の記事も参考にしてみてください。

Grape で Web API 開発 - kzy52's blog
Grape::Entity の使い方 - kzy52's blog

Rails プロジェクト作成

$ mkdir api-demo;cd api-demo
$ echo api-demo > .rbenv-gemsets
$ rbenv local 2.2.0
$ gem install bundler
$ gem install rails
$ rails new . --skip-sprockets

データベース作成

$ rake db:create

ユーザテーブルの作成

$ rails g model User name:string email:string
$ rake db:migrate

API 基盤の実装

gem の追加

# Gemfile
...

gem 'grape'
gem 'grape-entity'
$ bundle install

ディレクトリの作成

$ mkdir -p app/apis/api/v1
$ mkdir -p app/apis/entity/v1

app/apiをオートロードに追加

# config/application.rb

module ApiDemo
  class Application < Rails::Application
    ...
    # 以下を追加
    config.paths.add File.join('app', 'apis'), glob: File.join('**', '*.rb')
    config.autoload_paths += Dir[Rails.root.join('app', 'apis', '*')]
  end
end

API のルーティング追加

# config/routes.rb

Rails.application.routes.draw do
  mount API::Base => '/'
end

ベースクラスの作成

# app/apis/api/base.rb

module API
  class Base < Grape::API
  end
end

API のルーティングを確認するタスクの追加

# lib/tasks/routes.rake

namespace :api do
  desc 'API Routes'
  task routes: :environment do
    API::Base.routes.each do |api|
      method = api.route_method.ljust(10)
      path = api.route_path.gsub(':version', api.route_version)
      puts "     #{method} #{path}"
    end
  end
end

「rake api:routes」でgrapeで定義した API のルーティングを確認できます。

APIの実装

# app/apis/api/v1/base.rb

module API
  module V1
    class Base < Grape::API
      format :json
      default_format :json

      prefix 'api'
      version 'v1', using: :path
    end
  end
end
# app/apis/api/base.rb

module API
  class Base < Grape::API
    # 以下を追加
    mount V1::Base
  end
end
# app/apis/entity/v1/users_entity.rb

module Entity
  module V1
    class UsersEntity < Grape::Entity
      expose :id, :name, :email
    end
  end
end
# app/apis/api/v1/users.rb

module API
  module V1
    class Users < Grape::API
      resource :users do
        get do
          present User.all, with: Entity::V1::UsersEntity
        end
      end
    end
  end
end
# app/apis/api/v1/base.rb

module API
  module V1
    class Base < Grape::API
      ...
      mount V1::Users
    end
  end
end

データを入れて curl コマンドで API を叩いてみると以下のように返ってくると思います。

$ curl http://localhost:3000/api/v1/users
[{"id":1,"name":"user1","email":"user1@example.com"},{"id":2,"name":"user2","email":"user2@example.com"},{"id":3,"name":"user3","email":"user3@example.com"},{"id":4,"name":"user4","email":"user4@example.com"},{"id":5,"name":"user5","email":"user5@example.com"}]%

アプリ側実装

テンプレートは Single View Application で言語は Swift を選択してプロジェクトを作成します。 ここでは APIDemo というプロジェクト名にします。
Xcode はいったん閉じてください。

プロジェクトに移動する

先ほど作成した Xcode のプロジェクトに移動します。

$ cd APIDemo

Alamofire のインストール

今回はWeb APIをコールするので Alamofire/Alamofire · GitHub というHTTP通信用ライブラリ を使用します。

CocoaPods のインストール

CocoaPods はiOSライブラリ管理ツールです。
インストールされていない場合はインストールしてください。

$ gem install cocoapods

Podfileの作成

$ pod init
$ vim Podfile
# Podfile

source 'https://github.com/CocoaPods/Specs.git'

platform :ios, '8.0'
use_frameworks!

target 'APIDemo' do
  pod 'Alamofire', '~> 1.1'
end

target 'APIDemoTests' do

end

ライブラリのインストール

$ pod install

プロジェクトを開く

$ open APIDemo.xcworkspace

UITableView を使う準備

UITableView はテーブル表示にしたい場合に使用します。
今回はユーザ一覧の表示に使います。

// ViewController.swift

import UIKit

// UITableViewを使用する際はUITableViewDataSourceプロトコルとUITableViewDelegateプロトコルを実装する必要がある
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    let cellId = "MyCell"

    // 今回はテーブル表示にしたいので UITableView を使う
    var tableView : UITableView?

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        // 横幅、高さ、ステータスバーの高さを取得する
        let width: CGFloat! = self.view.bounds.width
        let height: CGFloat! = self.view.bounds.height
        let statusBarHeight: CGFloat! = UIApplication.sharedApplication().statusBarFrame.height
        
        self.tableView = UITableView(frame: CGRectMake(0, statusBarHeight, width, height - statusBarHeight))
        
        // デリゲートを指定する
        self.tableView!.delegate = self
        self.tableView!.dataSource = self
        
        // UITableViewにセルとして使うクラスを登録する
        self.tableView!.registerClass(UITableViewCell.self, forCellReuseIdentifier: cellId)
        
        // Viewに追加する。
        self.view.addSubview(self.tableView!)
    }
    
    // セルの総数を返す(表示するテーブルの行数)
    // UITableViewDataSource を使う場合は 必須
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // とりあえずは適当に返しておく
        return 1
    }
    
    // 表示するセルを生成して返す
    // UITableViewDataSource を使う場合は 必須
    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        // UITableViewCellはテーブルの一つ一つのセルを管理するクラス。
        let cell: UITableViewCell = tableView.dequeueReusableCellWithIdentifier(cellId, forIndexPath: indexPath) as UITableViewCell
        
        return cell
    }


    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

ユーザを保持するクラスの作成

  1. [APIDemo] を右クリック > [New Group] を選択しグループ名を 「Models」 します。
  2. [Models]を選択し[ユーティリティエリア] > [Identity and type]の フォルダアイコンをクリックする。
  3. [New Folder] をクリックし、ダイアログが表示されるので「Models」ディレクトリを作成します。
  4. [Choose] ボタンをクリックします。
  5. [Models]を右クリック > [New File...] > [iOS] > [Source] > [Swift File] を選択し [Next] ボタンをクリックします。
  6. ファイル名を「User.swift」にし3で作成した[Models]に保存します。
// Models/User.swift

import UIKit

struct User {
    var name: String, email: String
}

class UserDataManager: NSObject {
    var users: [User]

    // シングルトンにする
    // UserDataManager.sharedInstanceで常に同じインスタンスを取り出すことができる。
    class var sharedInstance : UserDataManager {
        struct Static {
            static let instance : UserDataManager = UserDataManager()
        }
        return Static.instance
    }

    override init() {
        self.users = []
    }

    // ユーザーの総数を返す。
    var size : Int {
        return self.users.count
    }

    // 配列のように[n]で要素を取得できるようにする。
    subscript(index: Int) -> User {
        return self.users[index]
    }

    func set(user: User) {
        self.users.append(user)
    }
}
// ViewController.swift

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    ...
    var tableView : UITableView?

    var users = UserDataManager.sharedInstance

    ...
    // セルの総数を返す(表示するテーブルの行数)
    // UITableViewDataSource を使う場合は必須
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.users.size
    }

    // 表示するセルを生成して返す
    // UITableViewDataSource を使う場合は必須
    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        // UITableViewCellはテーブルの一つ一つのセルを管理するクラス。
        let cell: UITableViewCell = tableView.dequeueReusableCellWithIdentifier(cellId, forIndexPath: indexPath) as UITableViewCell
            
        // Cellに値を設定する.
        let user: User = self.users[indexPath.row] as User
        cell.textLabel?.text = user.email
            
        return cell
    }
    ...
}

Web API へのリクエスト

[APIDemo] を右クリック > [New File...] > [iOS] > [Source] > [Swift File]を選択し、[Next]ボタンをクリックします。

[APIDemo] 配下に 「Router.swift」という名前で保存します。

ここは Alamofire/Alamofire · GitHub を参考にしました。

// Router.swift

import Alamofire

enum Router: URLRequestConvertible {
    static let baseURLString = "http://localhost:3000"
    
    case GetUsers()
    
    var URLRequest: NSURLRequest {
        let (method: Alamofire.Method, path: String, parameters: [String: AnyObject]?) = {
            switch self {
            case .GetUsers: return (.GET, "/api/v1/users", nil)
            }
        }()
        
        let URL = NSURL(string: Router.baseURLString)!
        let URLRequest = NSURLRequest(URL: URL.URLByAppendingPathComponent(path))
        let encoding = Alamofire.ParameterEncoding.URL
        
        return encoding.encode(URLRequest, parameters: parameters).0
    }
}
// ViewController.swift

import Alamofire

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    ...

    override func viewWillAppear(animated: Bool) {
        self.request()
    }

    // Web API をコールする
    func request() {
        Alamofire.request(Router.GetUsers()).responseJSON { (request, response, json, error) -> Void in
            if let json = json as? Array<Dictionary<String,AnyObject>> {
                for j in json {
                    var user: User = User(
                        name: j["name"] as NSString,
                        email: j["email"] as NSString
                    )
                    self.users.set(user)
                }
                
                dispatch_async(dispatch_get_main_queue(), {
                    self.tableView!.reloadData()
                })
            }
        }
    }
    ...

f:id:kzy52:20150325215141p:plain

ソースコード

github.com

参考ページ

Alamofire/Alamofire · GitHub