Hatena::ブログ(Diary)

naoyaのはてなダイアリー

December 21, 2012

RubyMotion で AWS iOS SDK を使う (もしくは Objective-C ライブラリの使い方、あるいはドラクエ10について)

ポロン「右手からRuby・・・」

ポロン「左手からiOS SDK・・・」

ポロン「合体!!!」

ポロン「魔力解放 RubyMotion!!!!」

・・・いやぁロト紋、懐かしいですね。こんにちは。みなさんドラクエ10やってますか!ぼくは全職カンストなのでやってません!あと数日で公開のパッチ1.2を待ち焦がれています。次のアップデートに期待してるのはなんといっても魔法戦士ですよね。以前にみたところによると魔法戦士にはMPを他のプレイヤーに分け与えるMPパサーという職業スキルがあって、パラディンのHPパサーが専用じゃなかったようにこれも非専用スキルのようです。と、いうことは!余ったMPを武闘家なり盗賊なりに分けて無限タイガークローなんてことが可能そう。これは熱い・・・当初予定のMPパサーの仕様に修正が入らなければ、ですけど。スティック100に振ってる自分としてはスーパースターがスティック装備可能だったのもグッと来ました。あとはインスタンスダ ・・・ ああ、すみません、RubyMotionの話でしたね、RubyMotion。

そんなわけで RubyMotion から AWS iOS SDK を使う、という話。RubyMotion Advent Calendar 2012 (http://www.adventar.org/calendars/18) の21日目の記事となりまーす。

RubyMotion とサードパーティライブラリ

RubyMotion と言えば BubbleWrap や SugarCube などの、Ruby like なシンタックスを強化する的なラッパー系ライブラリに注目が集まりがちです。先日も Clay Allsopp さんが例によって http://rubymotion-wrappers.com/ なページを公開していました。いいですね。

一方、膨大な既存資産である既存の rubygems がそのままでは使えない、というのが RubyMotion の残念なところのひとつともされます。そうですよね、自分は素の Ruby の方はあまり書かないのであれですけど、CPAN が使えない perl と言われたらそりゃあ難儀だと思わざるを得ない。

だがしかし! RubyMotion の拡張という意味で重要なのは BubbleWrap でも rubygems でもないのだよ諸君!

RubyMotion は Objective-C のシンタックスを Ruby とほぼ1対1でマッピングしているという関係上、既存の Objective-C なライブラリがほぼそのまま利用できるんです。iOS のアプリを開発するにあたって既存の rubygems のライブラリが使えることと、Objective-C のライブラリが使えることどっちが重要かと言われたら後者だという人は多いのではないでしょうか。複雑な GUI 系の実装とか、そういうのを既存の実装から流用できると考えれば。

Cocoapods と RubyMotion の素敵な関係

ところで Objective-C で外部ライブラリといえば、少し前までは、インターネット上の適当なサイトからダウンロードしてきて解凍して Xcode でプロジェクトに追加する・・・的なものでした。バージョン管理その他の機構は全くなく、その辺は手弁当な感じで。ものによってフレームワークの追加の仕方が違ったりして、ライブラリ一個使い始めるのにはまっちゃうなんてこともしばしば。いまどきそりゃないわ・・・と溜息をついたiOSアプリ開発者の数は天文学的数字に上るそうです。

そこで CocoaPods の登場です。

見てのとおり Cocoa 向け外部ライブラリの集積所。Bundler 的なフロントエンドを使って依存関係含め、インストール周りを管理できます。素敵! 自分が初めて Cocoapods について耳にしたのは 2011 年ころで、確かそのぐらいにリリースされたものだと記憶しています。当初は登録されているライブラリも多くなかったのでまだまだこれからという感じでしたが、結構な時間が経ってきてだいぶその辺も充実してきました。

で、RubyMotion。RubyMotion は Objective-C の外部ライブラリをほぼそのまま使えるという特性を活かして Cocoapods がバッチリ使えます。motion-cocoapods というフロントエンドを使って統合します。

見てのとおり HipByte が開発しているのが安心です。るびもコミッタの @watson1978 さんのアイコンが見えますね。

motion-cocoapods の使い方は非常に楽というか、CUI メインになっている RubyMotion ということもあって Objective-C で Cocoapods 使うときよりもさらに簡単です。

% gem install motion-cocoapods
% pod setup

でインストールして、RubyMotion アプリケーションの Rakefile に

$:.unshift("/Library/RubyMotion/lib")
require 'motion/project'
require 'motion-cocoapods'

Motion::Project::App.setup do |app|
  app.name = 'AWS'
  # この辺
  app.pods do
     pod 'AWSiOSSDK'
  end 
end

と書くだけ。Rakefile が Bundler でいうところの Gemfile、Cocoapods でいうところの Podfile、Carton でいうところの Makefile.PL に相当しています。引数でバージョンを指定したり、といったおなじみの機能もあります。

で、あとはいつも通りほのぼの rake すると vendor ディレクトリ内にライブラリ、上記の場合は AWSiOSSDK が展開されてビルドされる。これでサードパーティライブラリが提供する API を呼ぶことができるようになります。なにこれ簡単すぎる・・・。

RubyMotion から Amazon S3 に写真をアップロードするぜ!

ほんとにちゃんと動くのかね? ということで検証しましょう。AWS SDK for iOS (http://aws.amazon.com/jp/sdkforios/) を RubyMotion アプリから使ってみます。ローカルのフォトライブラリの写真を Amazon S3 にアップロードする、署名つきURLを発行してそれを閲覧するという二つの機能をさくっとSDKのサンプルからRubyMotionに移植してみましょう。

なお SDK の使い方は http://www.slideshare.net/AmazonWebServicesJapan/aws-sdk-for-android-and-ios にある AWS の皆さんのスライドが参考になりました。

で、ソースはこちら。

コントローラのコードを貼っておきましょう。

class S3UploadViewController < UIViewController
  ACCESS_KEY_ID = ""
  SECRET_KEY    = ""

  def viewDidLoad
    super

    self.navigationItem.title = "S3 Uploader in Motion"
    self.view.backgroundColor = UIColor.whiteColor

    @s3 = AmazonS3Client.alloc.initWithAccessKey(ACCESS_KEY_ID, withSecretKey: SECRET_KEY)
    response = @s3.createBucket(S3CreateBucketRequest.alloc.initWithName("s3uploader-motion-sample"))
    if (response.error != nil)
      alert(response.error)
    end

    UIButton.buttonWithType(UIButtonTypeRoundedRect).tap do |button|
      button.setTitle("Upload Photo", forState:UIControlStateNormal)
      button.frame = [[10, 100], [view.frame.size.width - 20, 40]]
      button.when(UIControlEventTouchUpInside) do
        showImagePicker
      end
      self.view.addSubview(button)
    end

    UIButton.buttonWithType(UIButtonTypeRoundedRect).tap do |button|
      button.setTitle "Open Photo", forState:UIControlStateNormal
      button.frame = [[10, 150], [view.frame.size.width - 20, 40]]
      button.when(UIControlEventTouchUpInside) do
        showInBrowser
      end
      self.view.addSubview(button)
    end
  end

  def showImagePicker
    UIImagePickerController.new.tap do |picker|
      picker.delegate = self
      self.presentModalViewController(picker, animated:true)
    end
  end

  def imagePickerController(picker, didFinishPickingMediaWithInfo: info)
    image = info.objectForKey(UIImagePickerControllerOriginalImage)
    processDispatchUpload(UIImageJPEGRepresentation(image, 1.0))
    picker.dismissModalViewControllerAnimated(true)
  end

  def processDispatchUpload(imageData)
    UIApplication.sharedApplication.setNetworkActivityIndicatorVisible(true)
    Dispatch::Queue.concurrent.async do
      req = S3PutObjectRequest.alloc.initWithKey("sample.jpg", inBucket: "s3uploader-motion-sample").tap do |r|
        r.contentType = "image/jpeg"
        r.data = imageData
      end

      response = @s3.putObject(req)

      Dispatch::Queue.main.sync do
        if (response.error != nil)
          alert(response.error)
        end
        UIApplication.sharedApplication.setNetworkActivityIndicatorVisible(false)
      end
    end
  end

  def showInBrowser
    Dispatch::Queue.concurrent.async do
      request = S3GetPreSignedURLRequest.new.tap do |r|
        r.key     = "sample.jpg"
        r.bucket  = "s3uploader-motion-sample"
        r.expires = NSDate.dateWithTimeIntervalSinceNow(3600)
        r.responseHeaderOverrides =  S3ResponseHeaderOverrides.new.tap { |o| o.contentType = "image/jpeg" }
      end

      err = Pointer.new(:object)
      url = @s3.getPreSignedURL(request, error:err)

      Dispatch::Queue.main.sync do
        if (url == nil)
          if (error[0] != nil)
            alert(error[0])
          end
        else
          App.open_url(url)
        end
      end
    end
  end

  def alert(msg)
    UIAlertView.new.tap do |v|
      v.message = msg
      v.show
    end
  end
end

例えば @s3.createBucket(…)" とかこの辺が SDK が提供する API です。いつも通り Objective-C → RubyMotion へのシンタックス変換ルールに従って呼ぶだけ。SDK がもともと Objective-C 向けの設計になってるので AWS SDK for Ruby に比べると Ruby らしくない API ですが、まあその辺は目をつぶりましょう。いざとなったら、それこそラッパを書いてしまえばいいわけです。

API コールによっては AWS に HTTP リクエストを発行してブロックするところがあるので、そこは GCD を使って並行処理しています。Dispatch::Queue で囲んでいるところですね。この辺がさくっと書けるのが良いですね。

実際動かしてみましょう。

f:id:naoya:20121221173630p:image:w320

こんな感じで立ち上がり

f:id:naoya:20121221173631p:image:w320

ピッカーで写真を選択すると、Amazon S3にアップロードされます。

f:id:naoya:20121221173632p:image:w320

署名付きURLを発行してブラウザで確認。ちゃんと表示されてますね。SDK は何の問題もなく動いているようです。やったぜ。

まとめ

というわけで、RubyMotion は既存のiOS開発資産が使えるところも良いですよ、というお話でした。

実は今回のネタは Objective-C で書かれたライブラリを悪戦苦闘の末導入してみてその顛末をまとめるというのを考えていたのですが、motion-cocoapods がおもいの他簡単に使えてしまい、しかも題材にしようとおもっていた AWS iOS SDK は Cocoapods に登録されていた・・・ということで悪戦苦闘は全くありませんでした。

Advent Calendar の中で @satococoa さんが、先日の Google Maps アプリで使われて話題になっている NimbusKit を motion-cocoapods で導入する記事を書かれていますね (http://satococoa.github.com/blog/2012/12/17/use-nimbus-css-with-rubymotion/)。そちらも参考にしてみてください。また Cocoapods に入っていないライブラリを使う場合は @watson1978 さんの Google Analytics SDK for iOS 導入記事 (http://watson1978.github.com/blog/2012/08/30/use-google-analytics/) が参考になるでしょう。

  • RubyMotion では Objective-C の既存資産が使えます
  • motion-cocoapods で Cocoapods が簡単に使えます
  • API 呼び出しはいつも通りの Objecitive-C ⇒ RubyMotion 変換ルールでOK
  • ドラゴンクエスト10のアップデート 1.2 はまもなくです。ウェーィ、スクエニちゃんマジカレオツゥ〜