ヽ(´・肉・`)ノログ

How do we fighting without fighting?

Evernoteのチュートリアルをrubymotionでやった

Evernote SDK for iOS クイックスタートガイドを RubyMotion でやる.

まだ記録として残している人はいないようだったのでここに記す.

1. 必要なもの

についてはRubyMotion特有のことがなにもないので省略する.

プロダクトの作成から.

rubymotion ではプロダクト作成を

motion create <プロダクト名>

で行える.そうするとプロダクトの雛形が展開される.

$ motion --version
2.22
$ motion create EvernoteTutorialWithRubyMotion
    Create EvernoteTutorialWithRubyMotion
    Create EvernoteTutorialWithRubyMotion/.gitignore
    Create EvernoteTutorialWithRubyMotion/app/app_delegate.rb
    Create EvernoteTutorialWithRubyMotion/Gemfile
    Create EvernoteTutorialWithRubyMotion/Rakefile
    Create EvernoteTutorialWithRubyMotion/resources/Default-568h@2x.png
    Create EvernoteTutorialWithRubyMotion/spec/main_spec.rb

2. SDKのダウンロードとインストール

Rubyのライブラリ管理ツールにRubyGemsやBundlerがあるように, Objective-Cのライブラリ管理ツールにCocoaPodsがある.

CocoaPodsではiOS用のEvernote-SDKが提供されている.

RubyMotionからはCocoaPodsを簡単に扱うためのライブラリmotion-cocoapodsというGemがある.

そこで

  1. motion-cocoapodsというGemのインストール
  2. motion-cocoapodsを利用したcocoapodsのインストール
  3. cocoapodsを利用したEvernote-SDKのインストール

を行い,RubyMotionからEvernote-SDKを利用できるようにする.

1. motion-cocoapodsというGemのインストール

Gemのインストールは

  1. BundleファイルにGemの依存関係を書く
  2. Bundleからinstallコマンドを実行する

と行なえる.

RubyMotionで利用するGemはRubyMotion特有のものが多い (例:motion-cocoapodsをCRubyやJRubyからは利用できない) ため,取得してくるGemはシステム全体が利用できる場所に置かず, プロジェクト内部で利用できる場所へ置くことにする.

具体的には

bundle install --path vendor/bundle

というようにコマンドラインオプションへ --path vendor/bundle を追加する.

こうするとvender/bundleというプロダクト内部のパスへGemがインストールされる.

vender/bundleをgit管理対象外にする

vender/bundle以下のファイルはコマンドを打つと 何回でも繰り返しインストールできるので, バージョン管理対象からは外しておく.

具体的には.gitignoreへ

vendor/bundle

を追記する.

2. motion-cocoapodsを利用したcocoapodsのインストール

motion-cocoapodsを利用してcocoapodsをインストールするには

rake pod:install

というコマンドを実行すればよい.今回はBundlerを利用しているので

bundle exec rake pod:install

となる.

3. cocoapodsを利用したEvernote-SDKのインストール

cocoapodsを利用してEvernote-SDKのインストールをするには

  1. Rakefileファイルにpodの依存関係を書く
  2. Rakeからpod:insallコマンドを実行する

と行なえる.

具体的にはRakefileへ

Motion::Project::App.setup do |app|
  # Use `rake config' to see complete project settings.
  app.name = 'EvernoteTutorialWithRubyMotion'
  app.pods do
    pod 'Evernote-SDK-iOS'
  end
end

というようにpodの依存関係の記述を追記して

bundle exec rake pod:install

するとvender/Pods以下にcocoapodsのライブラリがインストールされる.

vender/Podsをgit管理対象外にする

vender/Pods以下のファイルもコマンドを打つと 何回でも繰り返しインストールできるので, バージョン管理対象からは外しておく.

具体的には.gitignoreへ

vendor/Pods

を追記する.

3. SDK と OAuth の設定

motion-my_env

githubへpublicにソースコードを置くので秘密の情報をソースコードに書いてpushすることはできない.

通常のプログラムだと環境変数ENVを使うところだが,実機に転送した時に環境変数ENVは使えない.そこでmotion-my_envを利用する.

motion-my_envのREADMEに従いGemfileに

gem 'motion-my_env'

を追加.

Rakefileに

app.my_env.file = './config/environment.yaml'

を追加.

そして対象のファイルをgitには登録したくないので.gitignoreへ

config/environment.yaml

を追加しておく.

didFinishLaunchingWithOptions

アプリケーションのメイン部分で、didFinishLaunchingWithOptions メソッドを見つけます。このメソッドの最後の方(ただし return YES; の前)に以下のコードを追加します

app/app_delegate.rbの AppDaelegate#application(application, didFinishLaunchingWithOptions:launchOptions) へ APIキーを設定する.

Objective-Cのことをほとんど何も知らないので以下の例をrubymotionで書くとどうなるかわからない.

NSString *EVERNOTE_HOST = BootstrapServerBaseURLStringSandbox;
NSString *CONSUMER_KEY = @"your-key";
NSString *CONSUMER_SECRET = @"your-secret";

[EvernoteSession setSharedSessionHost:EVERNOTE_HOST
                          consumerKey:CONSUMER_KEY
                       consumerSecret:CONSUMER_SECRET];

そこでEmacsのmotion-modeに搭載されているmotion-convert-code-regionを利用してみる.

コードをリージョンで囲んでM-x motion-convert-code-regionすると以下のようにruby風のコードになる.一部おかしなところがあるがとっかかりとしては悪くない.

EVER NO TEがfalseになっているところは新鮮な驚きがあった.気持はわからないでもない.きっとObjective-Cではfalseの意味にNOを使ったりするのだろう.

EVERfalseTE_HOST = BootstrapServerBaseURLStringSandbox
CONSUMER_KEY = "your-key"
CONSUMER_SECRET = "your-secret"

EvernoteSession.setSharedSessionHost(EVERfalseTE_HOST, consumerKey:CONSUMER_KEY, consumerSecret:CONSUMER_SECRET)

を考慮した結果以下のようになる.

evernote_host = BootstrapServerBaseURLStringSandbox
consumer_key = MY_ENV['CONSUMER_KEY']
consumer_secret = MY_ENV['CONSUMER_SECRET']

EvernoteSession.setSharedSessionHost(evernote_host, consumerKey: consumer_key, consumerSecret: consumer_secret)

これをAppDelegate#application(application, didFinishLaunchingWithOptions:launchOptions)へ書き足す.

ヘッダーファイル

必ずEvernoteSession.h と ENConstants.h のヘッダーファイルを含めてください。

ヘッダーファイルはrubymotionでは不要なので何もしない.

application:openURL:sourceApplication:annotation

application:openURL:sourceApplication:annotation: メソッドを以下のように変更してください

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
    BOOL canHandle = NO;
    if ([[NSString stringWithFormat:@"en-%@", [[EvernoteSession sharedSession] consumerKey]] isEqualToString:[url scheme]] == YES) {
    canHandle = [[EvernoteSession sharedSession] canHandleOpenURL:url];
    }
    return canHandle;
}

よくわからないのでruby-motionのmotion-convert-code-regionに頼ってみる.

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
    BOOL canHandle = false
    if (NSString.stringWithFormat("en-%@",.EvernoteSession.sharedSession.consumerKey) isEqualToString(url.scheme) == true) {
    canHandle = EvernoteSession.sharedSession.canHandleOpenURL(url)
    }
    return canHandle
}

多少わかりそうになったもののまだよくわからない.これをヒントに判断していく.

メソッドシグネチャについて,RubyMotionの変換法則に従うと

(BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation

def application(application, openURL: url, sourceApplication: sourceApplication, annotation: annotation)

になるんじゃないかなあ.

返す値について,ソースコードを読むと,基本的にはfalseを返し,ある一定条件を満たす場合のみtrueを返すようにみえる.

条件とは何か.2つある.

1つめ

[[NSString stringWithFormat:@"en-%@", [[EvernoteSession sharedSession] consumerKey]] isEqualToString:[url scheme]] == YES

NSStrinng.stringWithFormat("en-%@", EvernoteSession.sharedSession.consumerKey).isEqualToString(url.scheme)

かなあ.

2つめ

[[EvernoteSession sharedSession] canHandleOpenURL:url];

EvernoteSession.sharedSession.canHandleOpenURL(url)

だろう.

つまり

def application(application, openURL: url, sourceApplication: sourceApplication, annotation: annotation)
  NSStrinng.stringWithFormat("en-%@", EvernoteSession.sharedSession.consumerKey).isEqualToString(url.scheme) &&
  EvernoteSession.sharedSession.canHandleOpenURL(url)
end

となる.

applicationDidBecomeActive

applicationDidBecomeActive: メソッドを EvernoteSession の handleDidBecomeActive メソッドを呼ぶように変更してください

- (void)applicationDidBecomeActive:(UIApplication *)application
{
[[EvernoteSession sharedSession] handleDidBecomeActive];
}

def applicationDidBecomeActive(application)
  EvernoteSession.sharedSession.handleDidBecomeActive
end

になるだろう.

4. アプリケーションの plist ファイルの更新

2.8. Advanced Info.plist Settingsによるとrubymotionではplistの設定をRakefileの中のapp.info_plistへ書くようだ.

先ほどと同じようにライブラリMY_ENVの値を利用したいところだが,Rakefileの中ではまだMY_ENVが利用できないようで

uninitialized constant MY_ENV

というエラーになる.

アドホックだがMY_ENVに用意したYAMLファイルを読み込んで利用することにする.

# -*- coding: utf-8 -*-
$:.unshift("/Library/RubyMotion/lib")
require 'motion/project/template/ios'
require 'yaml'

begin
  require 'bundler'
  Bundler.require
rescue LoadError
end

# Can't use MY_ENV in Rakefile
conf = YAML::load_file('./config/environment.yaml')

Motion::Project::App.setup do |app|
  # Use `rake config' to see complete project settings.
  app.name = 'EvernoteTutorialWithRubyMotion'
  app.pods do
    pod 'Evernote-SDK-iOS'
  end
  app.my_env.file = './config/environment.yaml'
  app.info_plist['CFBundleURLTypes'] = [
    {
      'CFBundleURLName' => ["en-#{conf['CONSUMER_KEY']}"]
    }
  ]
end

config/environment.yaml内に記載するCONSUMER_KEYには,EvernoteAPI申請時にもらったConsumer Keyを設定しておく. もしConsumer Keyがhogeだったとすると,CFBundleURLNameには’en-hoge’という値が入る.

5. Apple Framework の追加

が必要だと書いてある.

2.1. Common Optionsによるとrubymotionではframeworkの依存関係をRakefileの中のapp.frameworksへ書くようだ.

そこで

app.frameworks << 'StoreKit' << 'Security'

というのが必要……かと思った.

実際には,CocoaPods経由でEvernote-SDKをインストールするとEvernote-SDKの設定に書いてあるので作業は不要だった.

6. OAuth の検証

アプリのメインな UIViewController ヘッダーファイルを開き、以下の 2 行を追加します:

rubymotionではヘッダーファイル不要なので何もしない.

EvernoteのチュートリアルではInterfaceBuilderでボタンを用意して, そのボタンをタップしたときにテストされるようになっている.

今回はInterfaceBuilderを立ち上げるのが面倒なので, 直接コードでボタンを生成して,そのボタンへイベントをバインドするようにした.

ウィンドウとボタンの生成

今回使うコントローラーはTestAuthControllerという名前に決めた.ファイル名はtest_auth_controller.rbだ.

通常,rubymotionでコントローラーを置く場所はapp/controllersディレクトリとなっている.そこへ置く.

最初はapp/controllersディレクトリがないので作る.

app/controllers/test_auth_controller.rb は

class TestAuthController < UIViewController
  def viewDidLoad
    super
    @button = UIButton.buttonWithType UIButtonTypeRoundedRect
    @button.title = 'auth'
    @button.sizeToFit
    self.view.addSubview @button
  end
end

とする.このコントローラーをまだどこからも使っていないのでまだ何もならない.

このコントローラーを使うよう app/app_delegate.rb を変更する.

class AppDelegate
  def application(application, didFinishLaunchingWithOptions:launchOptions)
    evernote_host = BootstrapServerBaseURLStringSandbox
    consumer_key = MY_ENV['CONSUMER_KEY']
    consumer_secret = MY_ENV['CONSUMER_SECRET']

    EvernoteSession.setSharedSessionHost(evernote_host, consumerKey: consumer_key, consumerSecret: consumer_secret)

    @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
    @window.makeKeyAndVisible
    @window.rootViewController = TestAuthController.alloc.initWithNibName(nil, bundle: nil)
    true
  end
#(snip)

@window について書いた3行を追加した.

ここで

bundle exec rake simulator

するとあっさりした画面がでてくるはずだ.

evernote-test-auth-button.png

authを押してもイベントをつけていないのでまだ何もおこらない.

ボタンへのイベント登録

- (IBAction)testEvernoteAuth:(id)sender
{
    EvernoteSession *session = [EvernoteSession sharedSession];
    NSLog(@"Session host: %@", [session host]);
    NSLog(@"Session key: %@", [session consumerKey]);
    NSLog(@"Session secret: %@", [session consumerSecret]);

    [session authenticateWithViewController:self completionHandler:^(NSError *error) {
        if (error || !session.isAuthenticated){
            if (error) {
                NSLog(@"Error authenticating with Evernote Cloud API: %@", error);
            }
            if (!session.isAuthenticated) {
                NSLog(@"Session not authenticated");
            }
        } else {
            // We're authenticated!
            EvernoteUserStore *userStore = [EvernoteUserStore userStore];
            [userStore getUserWithSuccess:^(EDAMUser *user) {
                // success
                NSLog(@"Authenticated as %@", [user username]);
            } failure:^(NSError *error) {
                // failure
                NSLog(@"Error getting user: %@", error);
            } ];
        }
    }];
}

例によってコードをmotion-convert-code-regionしてみる.

- (IBAction)testEvernoteAuth:(id)sender {
    session = EvernoteSession.sharedSession
    NSLog("Session host: %@", session.host)
    NSLog("Session key: %@", session.consumerKey)
    NSLog("Session secret: %@", session.consumerSecret)

    session.authenticateWithViewController(self, completionHandler: -> error {)
        if (error || !session.isAuthenticated){
            if (error) {
                NSLog("Error authenticating with Evernote Cloud API: %@", error)
            }
            if (!session.isAuthenticated) {
                NSLog("Session not authenticated")
            }
        } else {
            // We're authenticated! EvernoteUserStore *userStore = EvernoteUserStore.userStore
            userStore.getUserWithSuccess(->user{)
                // success NSLog("Authenticated as %@", user.username)
            } failure(->error{)
                // failure NSLog("Error getting user: %@", error)
            }
        }
    }
}

うーん.何となくわかるような? Objective-Cでの ^ はRubyでの -> のようなものという予備知識があったので, 構文エラーもなんとか直せそうな気がする.

  def testEvernoteAuth
    session = EvernoteSession.sharedSession
    NSLog("Session host: %@", session.host)
    NSLog("Session key: %@", session.consumerKey)
    NSLog("Session secret: %@", session.consumerSecret)

    session.authenticateWithViewController(self,
                                           completionHandler: -> error {
                                             if (error || !session.isAuthenticated)
                                               if (error)
                                                 NSLog("Error authenticating with Evernote Cloud API: %@", error)
                                               end
                                               if (!session.isAuthenticated)
                                                 NSLog("Session not authenticated")
                                               end
                                             else
                                               # We're authenticated!
                                               userStore = EvernoteUserStore.userStore
                                               userStore.getUserWithSuccess(
                                                 -> user {
                                                   # success
                                                   NSLog("Authenticated as %@", user.username)
                                                 },
                                                 failure: -> error2 {
                                                   # failure
                                                   NSLog("Error getting user: %@", error2)
                                                 }
                                               )
                                             end
                                           })
  end
end

こんな感じにした.

これでauthボタンを押すとイベントが動作する.

7. Acidテスト

動作するようになっているはずなので試す.

config/environment.yaml.exampleをconfig/environment.yamlへリネームして,CONSUMER_KEYとCONSUMER_SECRETを自分が取得したもので書き換える.

$ bundle exec rake simulator

とコマンドを打つとシミュレータが起動する.

authボタンを押すと,Evernoteのログイン画面になる.

アカウントとパスワードを入力して正しくログインすると,コンソール画面に

2014-03-15 06:49:55.592 EvernoteTutorialWithRubyMotion[82019:70b] Authenticated as xxx

といったようなログが出てくるはずだ.これが出れば成功だ.

このエントリーをはてなブックマークに追加