24/7 twenty-four seven

iOS/OS X application programing topics.

iOS 8 beta5にてポップオーバーをキャンセルするための暗いところを連続でタップすると下にあるモーダルビューも閉じてしまうバグ

一昨日くらいにiOS 8でテストをしていたら見つけました。
問題となる画面構成を多用するアプリケーションにとっては、修正が間に合わなければけっこう致命的と思われるので共有します。
(Base SDKをiOS 8にしてビルドしない限りは起こりません。リリース済みのアプリケーションをiOS 8で動かすぶんには問題無いです。)

↓ バグレポートした内容は下記の記事で公開しています。
重ねてレポートすると優先順位があがるかもしれません。

Radar: When double-tap the dimming view to dismiss the popover, it will also close modal view under - 24/7 twenty-four seven

サンプルコード

↓ バグレポートに添付した問題が再現するサンプルコードです。
Xcode 6でビルドして、iOS 8のiPadで実行すると問題が再現できます。

https://dl.dropboxusercontent.com/u/285673/sample.zip

事象について

事象を簡単にまとめると、モーダルビューの上にポップオーバーを表示している時、暗いところをタップしたらポップオーバーが消える、のが通常だけど、それを2回以上タップすると、下のモーダルビューまで閉じてしまう。

調べると、下のビューのdismissViewController〜メソッドが呼ばれることでポップオーバーが閉じるのだけど、
ダブルタップ以上すると、それが複数回呼ばれるので、ポップオーバー以外のビューも閉じられてしまう、という理屈です。


20140831200047

具体的な影響

このダブルタップの間隔は暗くなっているビューのalphaがゼロになるまでにすれば再現するので、かなりゆっくりでも起こってしまいます。
ちょっと反応が悪いかな?と思ってもう一回タップすると画面全体が消えてしまった、みたいなことが普通に起こります。


やっかいな点は、ポップオーバー形式で表示するビューは何でもこの問題の対象となることです。
iPadでは自動的にポップオーバーで表示されてしまうものや、ポップオーバーで表示しなければならない標準のビューはけっこうあって、UIActionSheetやUIImagePickerController、共有につかうUIActivityViewController、UIDocumentInteractionControllerなどが全部対象になります。


↓ 例えば、下のような画面でモーダルビューでUIWebViewを表示して、そこに共有のためのボタンがある、みたいな画面も対象になります。
(下の状態で暗いところをダブルタップすると、UIWebViewを表示しているモーダルビューも閉じてしまう)


20140831203642


救いなのは、Xcode 6つまりBase SDKをiOS 8にしてビルドしない限りはこの問題は起こらないことです。
(リリース済みのアプリケーションをiOS 8で使うぶんには問題ない)


ただ、満を持してiOS 8のリリースと同時にアップデートしたアプリケーションで問題が起きたらいろいろ大変だと思うので、iPad対応アプリケーションを出してるひとは気をつけてください。

対策

もし、GMで修正されていなかったときのためにいちおう対策を考えました。
標準のUIActionSheetとかで起こるので、呼び出し側で工夫するのは限界があるので、問題の起こっている、暗いところをタップしたときのイベントハンドラをSwizzlingして同じインスタンスの呼び出しなら1度しか実行しない、というようにしてみました。

正直、こういうコードをプロダクトに入れたくはないので、修正が間に合うことを願っています。

static IMP __Original_UIPopoverPresentationController_dimmingViewWasTapped;

void __Swizzle_UIPopoverPresentationController_dimmingViewWasTapped(id self, SEL _cmd, id sender)
{
    static id dimmingView;
    if (dimmingView == sender) {
        return;
    }
    dimmingView = sender;
    __Original_UIPopoverPresentationController_dimmingViewWasTapped(self, _cmd, sender);
}

+ (void)load
{
    Class clazz = NSClassFromString(@"UIPopoverPresentationController");
    SEL selector = @selector(dimmingViewWasTapped:);
    Method method = class_getInstanceMethod(clazz, selector);
    __Original_UIPopoverPresentationController_dimmingViewWasTapped = method_setImplementation(method, (IMP)__Swizzle_UIPopoverPresentationController_dimmingViewWasTapped);
}

Radar: When double-tap the dimming view to dismiss the popover, it will also close modal view under

If you display a popover on a modal view, when performing multiple tap the dimmig view to close popover, modal view will also be closed not just popover.

Tapping dimming view calls the event handler(-[UIPopoverPresentationController#dimmingViewWasTapped:]).
The method calls presenting view controller's `dismissViewController:animated:completion:` method.
So the method called twice or more, `dismissViewController~` method is called multiple times, also close other views not just pop over.

Submitted as rdar://18186956 and to OpenRadar.

Summary:

If showing a pop over on the modal view, double-tap the dimming view to dismiss the popover.
It will also close the modal view together.

Steps to Reproduce:
  1. Open and run the project https://dl.dropboxusercontent.com/u/285673/sample.zip
  2. Push the right bar button (named "Modal") on navigation bar
  3. Shown the table view modally
  4. Push the right bar button (named "Popover") on navigation bar, shown popover
  5. "Double-Tap" the dimming view
Expected Results:

Dismiss the popover

Actual Results:

Dismiss the popover, and also dismiss modal view under

Regression:

iPad Air, 32GB, WiFi
iOS 8.0 (12A4345d)

Note:

Not only UIPopoverController, this occurs in all views to be displayed in a pop-over style.
For example UIAlertControler(ActionSheet), UIPrintInteractionController, and so on.

Travis CI (Pro) の実行をジョブの並列化とBundlerとCocoaPodsのキャッシュで速くした

ユビレジではTravis CIを使って、テストの実行とベータ版のTestFlightへのアップロードを自動化しています。

Pull Requestが送られた時と、マージされた時に自動でマージした結果のベータ版が配布されるので、手元で変更をすぐに試すことができて便利です。


【参考】
Travis CIでiOSアプリのテスト&ベータ版の配信に使っているRakefileを改善したメモ - 24/7 twenty-four seven
ユビレジのiPadアプリのCI環境をJenkinsからTravis CIに移行したときのまとめ - 24/7 twenty-four seven


ただ、これは導入当初からあった問題なのですが、Travis CIにジョブが登録されてから終了するまで、だいたい20〜25分くらいと、少し時間がかかるのが気になっていました。


そこでジョブの並列化と、BundlerとCocoaPodsのキャッシュ2点で高速化を試みました。

まずジョブの並列化は、テストの実行とTestFlightへのアップロードを分けて並列で実行することにしました。
これまでテストがすべて成功した後に再度デバイス用のバイナリをビルドしてTestFlightにアップロードしていました。

それをテストの終了を待たずに、TestFlightのアップロードをするように変更しました。

テストが失敗するビルドがTestFlightで配信されることが起こりますが、テストが通らないビルドがマージされることは無いので、手元で早く確認できるということを優先しました。


変更は簡単で.travis.ymlの呼び出し方法を少し直すだけでした。

↓ もともとこうだったのが、

language: objective-c
script:
- bundle exec rake test
after_success:
- bundle exec rake profile:download
- bundle exec rake certificate:add
- bundle exec rake distribute
- bundle exec rake certificate:remove


↓ まず、Rakeタスクの呼び出しをそれぞれ1行にします。

language: objective-c
script:
  - bundle exec rake test
after_success:
  - bundle exec rake profile:download certificate:add distribute certificate:remove


↓ タスクをBuild Matrixのパラメータに振り分けます。

language: objective-c
script:
  - bundle exec rake ${ACTION}
env:
  matrix:
    - ACTION='profile:download certificate:add distribute certificate:remove'
    - ACTION=test


これだけで、Matrixの上段はTestFlightの配信、下段はテストの実行で並列に実行されます。



次にBundlerとCocoaPodsをキャッシュする設定です。

↓ OSXのビルド環境でもキャッシュの設定が有効になったということで試しました。

The Travis CI Blog: Caching (all the things) on the Mac platform

language: objective-c
cache:
  - bundler
  - cocoapods


↑ のオーソドックスな設定ではなぜかうまく動かなかったので、下のようにディレクトリをキャッシュする設定を使いました。

language: objective-c
cache:
  directories:
    - vendor/bundle
    - Pods
install:
  - bundle install --path=vendor/bundle --binstubs=vendor/bin
  - bundle exec pod install


ビルド時間の半分以上がBundlerとCocoaPodsのインストールにかかる時間だったので、これはかなり有効でした。


最終的に、TestFlightへのアップロードが3〜5分、テストの実行が4〜6分くらいで合計で8〜12分くらい、並列で実行されるのでPull Requestしてからだいたい5分くらいで手元で試せるベータ版が届くという環境になっています。


iOS 8でIn-App Purchaseの状態に追加されるSKPaymentTransactionStateDeferredの影響を考える

In-App Purchaseでプロダクトの購入を扱うときにはStoreKitのSKPaymentTransactionStateを使います。
例えばPurchasedなら購入完了なのでプロダクトのダウンロードを始める、Failedなら失敗なのでアラートを出すなどとします。

iOS 8からはその状態に新しくSKPaymentTransactionStateDeferredが追加されます。


2014/8/6時点ではまだドキュメントに解説はありません。
API diffとヘッダには記載されています。
WWDCのセッション218, 303ではそれなりに詳しく解説されていますので参考になると思います。


Deferredという状態は「Ask to Buy」というiOS 8のApp Storeの新機能のために導入されました。
「Ask to Buy」はiOS 8で搭載される「ファミリー共有 (Family Sharing)」の1機能で、子どもがApp Storeから購入する際に親の許可を必要とすることができるというペアレンタルコントロールの強化です。

20140806155004 20140806155004

Apple - iOS 8 - ファミリー共有


「Ask to Buy」が有効になっているアカウントで購入しようとしたとき、購入をいったん保留して許可を求める通知が親のアカウントに届きます。
このとき、購入は「完了(Purchased)」でも「失敗(Failed)」でもなく「Deferred」になります。


いずれDeferredは「完了(Purchased)」か「失敗(Failed)」のどちらかになりますが、それがいつになるかは親のアカウントがいつ判断をするかによるので、場合によっては何日かかかることも考えられます。
そのため、セッション303によると、この状態のときは通常通り(購入前の状態で)アプリケーションが使えるようにしなければならないとのことです。


さて、上記を踏まえて、既存のアプリケーションについてどのような対応が必要になるか考えてみます。


通常In-App Purchaseをハンドリングするオーソドックスな実装は下記のようになっていると思います。

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
    for (SKPaymentTransaction *transaction in transactions) {
        switch (transaction.transactionState) {
            case SKPaymentTransactionStatePurchasing:
                break;
            case SKPaymentTransactionStatePurchased:
                [self sendReceipt:transaction];
                [self completeTransaction:transaction];
                break;
            case SKPaymentTransactionStateFailed:
                [self failedTransaction:transaction];
                break;
            case SKPaymentTransactionStateRestored:
                [self restoreTransaction:transaction];
                break;
            default:
                break;
        }
    }
}


そして、購入を開始してから終了(完了または失敗)までをモーダルに処理しているアプリケーションも多いと思います。
(そのほうがわかりやすいことも多いので、購入開始〜完了までをモーダルにするのは別に悪いUIでは無いと思います)


ただ、その場合は上記の実装をそのままにしておくと、iOS 8でSKPaymentTransactionStateDeferredが返ってきたときに完了にも失敗にもならないので、購入中のモーダルな状態のままになってしまうことになります。

そこで、上記のswitch文にSKPaymentTransactionStateDeferredの分岐を追加します。
おそらくいったん失敗(Failed)と同じ処理をするのが簡単じゃないかと思います。
ただし、おそらく失敗のときとは異なり、トランザクション自体は終了させない(+ [SKPaymentQueue finishTransaction:]を呼ばない)のではないかと思います。
(このあたり、Sandbox環境で試すことはまだできないようですし、ドキュメントも無いので実際にどうなのかは不明です)


もともと、購入中でもモーダルにしてなくて自由に通常の操作ができる、という作りのアプリケーションの場合は特に気にせずゆっくり対応しても大丈夫そうかなと思います。
Deffered以外の既存の購入フローには影響はなさそうなので。

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
    for (SKPaymentTransaction *transaction in transactions) {
        switch (transaction.transactionState) {
            case SKPaymentTransactionStatePurchasing:
                break;
            case SKPaymentTransactionStatePurchased:
                [self sendReceipt:transaction];
                [self completeTransaction:transaction];
                break;
            case SKPaymentTransactionStateFailed:
                [self failedTransaction:transaction];
                break;
            case SKPaymentTransactionStateRestored:
                [self restoreTransaction:transaction];
                break;
            case SKPaymentTransactionStateDeferred: 
                // Deferredの状態に対応する処理を追加する
                // おそらく「失敗(Failed)」と同様にするのが簡単
                // ただし、トランザクションは終了させない(と思う)
                break;
            default:
                break;
        }
    }
}


「ファミリー共有 (Family Sharing)」および「Ask to Buy」の機能がいつ、既存のアプリケーションに対して適用されるのかとかもよくわかりませんが、In-App Purchaseを利用している場合は何らかの対応を検討する必要がありそうだなと思います。

Storyboardを1画面ごとに分割した話

今年の5月くらいの話なのですが、ユビレジのiPadアプリケーションのプロジェクトで使っているStoryboardを基本的に1画面(≒1 View Controller)の単位に分割するということをしました。


1画面1Storyboardメソッドについてはnakiwoさんが書かれた記事も参考になります。

1画面から始めるStoryboard - Cocoaメモ


↑ 上記の資料はどちらかというとStoryboardを使い始めるにあたって、1画面単位で少しずつ使っていこうという感じですが、ユビレジではもともとほぼ全部の画面がStoryboardになっていました。

ただ複数人で共同作業をするにあたっては、1画面単位を1ファイルにしておくくらいがメンテナンスしやすいんじゃないかなあという結論になったのでしばらくそういうふうに運用することにしました。


また、XIBと違ってStoryboardは単純にコピー&ペーストで別のファイルにまったく同じ構成のものを移すことができるので試すのも戻すのも非常に簡単なので合わなかったら戻せばよいと考えました。
(XIBだと単純なコピー&ペーストだとアクションの関連が外れたりします。まあXIBに複数のコンポーネントがあってそれを分けたいということは滅多にないのでそれは特に問題ではないですが)


複数のView Controllerを含む大きなStoryboardに比べて、小さなStoryboardのメリットしては、

  • コンフリクトの可能性が減る(大きなStoryboardだと、全く別の画面の修正にもかかわらず同じファイルになるので)
  • Auto LayoutのOn/Offを画面ごとに柔軟に設定できる(Auto Layoutの設定はファイルごとなので)
  • XIBと同様に使える(StoryboardはXIBの上位互換)

があります。

あと大きなStoryboardは作った当人は慣れているので、どこに何があるかすぐ分かるけど、他のひとにとっては目的の画面を探すのもけっこう大変だったりします。


特にユビレジでは4/5月にiOSのエンジニアが増えたので、Storyboardのマージに気を使うことが多くなることを避けたいというのが第一の大きな理由で、次に新しい人が既存のコードを追っていくなかでStoryboardを見るときに迷わないようにしたいという点です。

デメリットとしては、基本的に1つのStoryboardファイルには1つの画面しかないので、画面の遷移にSegueを利用することはできなくなります。

ただその点については、Segueを使うこと自体がそれほどメリットではないということで、トータルで考えるとそれほどデメリットにはならないという判断をしました。


Segueを使う場合であまり良くないと考えていることの1つは、多くのケースでは画面を遷移するときにいくつかデータを受け渡す必要があり、Segueを使う場合は標準のAPIではその部分がどうしても微妙になってしまうという点です。


例えば典型的なSegueを使った画面の遷移は下記のようになります。

- (IBAction)nextButtonTapped:(id)sender
{
    [self performSegueWithIdentifier:NSStringFromClass([UBCheckoutViewController class]) sender:self];
}

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([segue.identifier isEqualToString:NSStringFromClass([UBCheckoutViewController class])]) {
        UBCheckoutViewController *controller = segue.destinationViewController;
        controller.delegate = self;
        controller.checkout = self.checkout;
    }
}

↑ このように、ユーザーのアクションを受けて次の画面に必要なパラメータを渡して遷移するというごく普通のケースを記述するのに、画面を遷移するという処理と、別の処理で遷移先を判断してパラメータをセットするという2つに少なくとも分かれてしまいます。
さらに、Storyboard上で設定するSegueのIDがコードに文字列として(!)現れてしまうというのも残念なところです。


いちおう、SegueのIDは遷移先のビューコントローラの名前と同じにするというルールにして、コード上ではクラス名から取得するという形で、リファクタリングなどがある程度有効になるようにしていますが、Storyboard上でIDを修正し忘れたり、両方を一緒に修正しなければならないというのは結局変わらないので、やっぱりイマイチだなと思います。


1画面1Storyboardの場合は、従来の遷移先のビューコントローラを作ってデータをセットして遷移という形式で書くことができます。

- (IBAction)nextButtonClicked:(id)sender
{
    UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Checkout" bundle:nil];
    UBCheckoutViewController *controller = [storyboard instantiateInitialViewController];
    controller.delegate = self;
    controller.checkout = self.checkout;
    
    [self presentViewController:controller animated:YES completion:NULL];
}


↑ 上記の場合でもUBCheckoutViewControllerをインスタンス化するには「Checkout」という名前のStoryboardを使うという遷移元のビューコントローラが特に知る必要のない情報が書かれてしまうのが残念なので、少し工夫して下記のように、Storyboardからインスタンス化する部分をそのビューコントローラのクラスメソッドに隠蔽しています。
(ちなみにStoryboardのファイル名もビューコントローラ名にすればいいかと悩みましたが、厳密に1画面だけにするべき、とはしたくなかったのでそれはやめました)

- (IBAction)nextButtonClicked:(id)sender
{
    UBCheckoutViewController *controller = [UBCheckoutViewController checkoutViewController];
    controller.delegate = self;
    controller.checkout = self.checkout;
    
    [self presentViewController:controller animated:YES completion:NULL];
}


初期データを設定するだけのためにプロパティを公開するのは嫌だ、という場合にはイニシャライザで渡すように書くのも簡単です。

- (IBAction)nextButtonClicked:(id)sender
{
    UBCheckoutViewController *controller = [UBCheckoutViewController checkoutViewControllerWithCheckout:self.checkout delegate:self];
    
    [self presentViewController:controller animated:YES completion:NULL];
}


こうしておけば、UBCheckoutViewControllerの作成方法は使う側には関係がないので、仮にStoryboardを使わずにコードで生成するように変わったとしても使う側に影響はありません。


他社のプロジェクトで私が見た例だと、Factoryクラスを用意してそれを通してView Controllerを取得するように作っているところもよく見ます。
もちろんケースバイケースですが、1クッション挟むわりには特に抽象度も結合度もそれほど変わらないと思ったので、それなら直接相手のView Controllerを使うほうがわかりやすいと考えて上記の方法を採用しています。


余談ですが、下記の所さんの記事など、ヘルパーライブラリを作って解決する方法もあります。
ただ、我々は画面の遷移という基本的な処理を理解するのに標準でないAPIをまず知らなければならないという状況は、少なくとも人の変化がこれからも頻繁に起こることが考えられる中では避けたいと判断しました。

Storyboardでの画面遷移をスマートにやる方法 - TOKOROM BLOG


また、少なくとも私は上記の記事にある遷移元に遷移先の情報が必要になる(依存関係がある)という状況は特に問題とは考えていないので(逆はかなり問題があります)先に書いたくらいがバランスいいのではと思っています。


別にSegueはダメな子なので使うなというわけではもちろんありません。
新しいものをイチから作っていくときなど、試行錯誤が必要なときで一人で作るぶんには1つのStoryboardでSegueを使っていくのはとても早く作れるので非常に便利ですし、私も新しいものはガンガンSegueで繋いで作っています。


ただ、ある程度UIが落ち着いてきて、細かい改修だったり共同作業が増えてきた段階になると、
細かい単位にStoryboardを分けることで変更がやりやすくなるかどうかを検討してもいいんじゃないかという話です。


参考までに分割前と、後のスクリーンショットを載せておきます。


分割前:
(Storyboardはこれを含めて3つあった中でメインに使っていたものです)
20131117181446


分割後:
(基本的には1画面1Storyboardですが、一部の設定画面やモーダルビューは関連する画面をまとめたまま残しています)

20131117181446

20131117181446

20131117181446

20131117181446

20131117181446

Travis CIでiOSアプリのテスト&ベータ版の配信に使っているRakefileを改善したメモ

↓ コード署名に失敗する問題を直すついでに、今まで運用していく中でいくつか改善したい点があったので少し手を加えました。
Travis CIでipaを作るときのCode Signが失敗するのを修正したメモ - 24/7 twenty-four seven

Provisioning Profileのダウンロードにnomad/cupertinoを使う

これまではapple-devというRubyのスクリプトを使用していたのですが、それをやめて、cupertinoというGemを使うようにしました。


もともとcupertinoの存在は知っていたのですが、READMEを見て対話的な使い方しかできないものだと思ってしまっていたので、CIでは使えないと勘違いしていました。


実はコードを読んでみると、次のように`-u`と`-p`オプションでユーザ名とパスワードを渡すことがどのコマンドについてもサポートされているようだったので試してみるとあっさり成功したのでこれを使うように置き換えました。

global_option('-u', '--username USER', 'Username') { |arg| agent.username = arg unless arg.nil? }
global_option('-p', '--password PASSWORD', 'Password') { |arg| agent.password = arg unless arg.nil? }
global_option('--team TEAM', 'Team') { |arg| agent.team = arg if arg }


apple-devはBundlerでインストールできるようになっていなかったので、リポジトリにスクリプトを含めていましたが、それが不要になったので少しリポジトリがスッキリしました。

Pull Requestに対するビルドについてもTestFlightでベータ版を配信する

これまではmasterブランチにマージしたときだけTestFlightでベータ版を配信していたのですが、Pull Requestを簡単に動かして確認したいという声があったので、試しにPull RequestについてもTestFlightにアップロードするようにしてみました。


↓ この対応についてはTravis CIの実行時に設定される環境変数を見て処理を判断していたところを消しただけです。

task :testflight => ["version:set_build_version", IPA_FILE, :crittercism] do |t|
  iff ENV['TRAVIS_PULL_REQUEST'] != "false"
    puts "This is a pull request. No deployment will be done."
    next
  end
  if ENV['TRAVIS_BRANCH'] != "master"
    puts "Testing on a branch other than master. No deployment will be done."
    next
  end


また、Travis CIにはPull Requestの場合`TRAVIS_PULL_REQUEST`という環境変数にPull Requestの番号を入れてくれてるということなので、ビルド番号にPull Requestの番号を加えて配信することにしました。

task :set_build_version do
  rev = `git rev-parse --short HEAD`.strip

  pr_number = ENV["TRAVIS_PULL_REQUEST"];
  rev << " ##{pr_number}" if pr_number != "false"

  puts "Setting build version to: #{rev}"

  InfoPlist.build_version = rev
end

↑ これで、Pull Requestから作られたビルドについては、"Ubiregi2 2.67 (5bf4bc9 #590)"のような形でPull Requestの番号が表示されます。

Travis CI Pro(有料版の環境)のRubyが2.0.0でもCocoaPodsの実行に失敗しなくなった

今まではTravis CI Proの環境だとRuby 2.0.0を使うと`pod install`に失敗していたので、わざわざRuby 1.9.3の環境を指定していたのですが、それが直ったのでRuby 2.0.0を使うことができるようになりました。

↓ それで何が良くなったかというと、Rakefileでキーワード引数が使えるようになったので少しシンプルに書けるようになりました。

def join_option(options: {}, prefix: "", seperator: "")
  _options = options.map { |k, v| %(#{prefix}#{k}#{seperator}"#{v}") }
  _options = _options.join(" ")
end


↓ これらの変更を加えたRakefileの全体はこちら
Rakefile for testing, building and uploading to Testflight/Crittercism

Travis CIでipaを作るときのCode Signが失敗するのを修正したメモ

一週間ほど前から(おそらくTravis CIの環境がXcode 5.1に変わってから)Travis CI上でipaファイルの作成に失敗するようになってしまって、TestFlightにベータ版を自動的にアップロードすることができなくなっていたのを昨日ようやく直したのでメモ。


↓ということで以前に書いた記事はちょっと古くなってしまいました。
本文はそのままですが、参照先のgistの内容はアップデートしてあります。
ユビレジのiPadアプリのCI環境をJenkinsからTravis CIに移行したときのまとめ - 24/7 twenty-four seven


失敗している箇所のエラーメッセージは下記の通り。ipaを作る前の、プロジェクトのビルドでコード署名をするところで失敗しているけど、これだけだと原因がよくわからないのでまず手元で同様のメッセージが出る状況を再現することを実行しました。

&#8998; Code Sign error: No matching codesigning identity found: No codesigning identities (i.e. certificate and private key pairs) matching “iPhone Distribution: Ubiregi Inc. (Y7522692LT)” were found.
&#8998; CodeSign error: code signing is required for product type 'Application' in SDK 'iOS 7.1'

** BUILD FAILED **
The following build commands failed:
Check dependencies
(1 failure)
rake aborted!

Command failed with status (65): [xcodebuild -sdk "iphoneos" -workspace "/Us...]


で、手元で新しくプロジェクトを作って状況を変えながら(Provisioning Profileを無くしてみる、秘密鍵をキーチェーンから消してみる、など)コマンドラインでビルドするのを何回かして、キーチェーンに秘密鍵が存在しないときに同様のエラーになることがわかりました。


↓そうすると、下記のキーチェーンに秘密鍵をインポートしてる部分がちゃんとできていないのだろうというところまではすぐにわかったのですが、直すのはよくわからなくて、けっこう時間がかかってしまいました。
というのも手元だと成功してしまうので、ちょっと修正してPushして結果を見て、というのを繰り返す必要がありました。

def add_certificates
  sh "security create-keychain -p travis #{KEYCHAIN_NAME}"
  sh "security import ./certificates/apple.cer -k ~/Library/Keychains/#{KEYCHAIN_NAME} -T /usr/bin/codesign"
  sh "security import ./certificates/dist.cer -k ~/Library/Keychains/#{KEYCHAIN_NAME} -T /usr/bin/codesign"
  sh "security import ./certificates/dist.p12 -k ~/Library/Keychains/#{KEYCHAIN_NAME} -P #{KEY_PASSWORD} -T /usr/bin/codesign"
  sh "mkdir -p \"#{PROFILE_DIR}\""
  sh "cp \"./profiles/#{PROFILE_NAME}.mobileprovision\" \"#{PROFILE_DIR}\""
end


↓結局、下記のように`security list-keychains -s #{KEYCHAIN_NAME}`の1行を追加すると成功するようになりました。

def add_certificates
  sh "security create-keychain -p travis #{KEYCHAIN_NAME}"
  sh "security import ./certificates/apple.cer -k #{KEYCHAIN_NAME} -T /usr/bin/codesign"
  sh "security import ./certificates/dist.cer -k #{KEYCHAIN_NAME} -T /usr/bin/codesign"
  sh "security import ./certificates/dist.p12 -k #{KEYCHAIN_NAME} -P #{KEY_PASSWORD} -T /usr/bin/codesign"
  sh "security list-keychains -s #{KEYCHAIN_NAME}"
end


`list-keychains`はキーチェーンのサーチリストを表示するコマンドですが、`-s`オプションでサーチリストに指定のキーチェーンを追加することができるので、それが有効なのかなと思いました。
これでしばらく運用していましたが、前の記事のコメントid:gin0606さんに以下の情報を教えていただきました。


XCode code signing issue with OS X Mavericks images


↑ のあさりさんの情報によると次の2行を追加して、作成したキーチェーンをデフォルトに指定することと、アンロックすれば直るとのことでした。

security default-keychain -s ios-build.keychain
security unlock-keychain -p travis ios-build.keychain 


アンロックは試してたのですが、デフォルトにするのは試してなかったのでやってみました。
結果は、デフォルトにするのは効果があり、アンロックは特に必要ありませんでした(あってもなくてもよい)。


どちらでも良さそうですが、デフォルトに指定するほうがなんとなく気に入ったので、それを採用しました。
↓ ということで、今は下のようなコードで正常に動いています。

def add_certificates
  sh "security create-keychain -p travis #{KEYCHAIN_NAME}"
  sh "security import ./certificates/apple.cer -k #{KEYCHAIN_NAME} -T /usr/bin/codesign"
  sh "security import ./certificates/dist.cer -k #{KEYCHAIN_NAME} -T /usr/bin/codesign"
  sh "security import ./certificates/dist.p12 -k #{KEYCHAIN_NAME} -P #{KEY_PASSWORD} -T /usr/bin/codesign"
  sh "security default-keychain -s #{KEYCHAIN_NAME}"
end


↓ これらの変更を加えたRakefileの全体はこちら
Rakefile for testing, building and uploading to Testflight/Crittercism


↓ .travis.ymlの全体はこちら
.travis.yml for Ubiregi iPad App

NSArrayやNSDictionaryからNSNullを効率よく取り除く

iOSアプリケーションでWeb APIから返ってきたJSONを処理するのにNSNullの扱いに困っていて、事前にNSNullを取り除いてしまうのが事故を防ぐための確実な方法なのですが、再帰的にすべての要素を検査する以外になにかいい方法がないかと思って考えていたらちょっとおもしろい方法を思いついたので書いてみました。


kishikawakatsumi/CollectionUtils · GitHub

↑ に含まれるCUCompactArrayとCUCompactDictionaryです。


NSArrayとNSDictionaryのサブクラスとして実装されていて、次のようにして生成します。
(普通にalloc/initを使って生成することも可能です)

NSArray *array = @[@"0", @"1", [NSNull null], @"2", [NSNull null], @"3"];
NSArray *compactArray = [array cu_compactArray];
//=> ["0", "1", "2", "3"]
NSDictionary *dictionary = @{@"one": @"1",
                             @"null": [NSNull null],
                             @"two": @"2",
                             @"three": @"3"};
NSDictionary *compactDictionary = [dictionary cu_compactDictionary];
 //=> {"one": "1", "two": "2", "three": "3"}


JSONオブジェクトに対して使われることを想定しているので、ネストしたコレクションに対しても有効です。

NSArray *array = @[@"0",
                   @"1",
                   [NSNull null],
                   @"2",
                   @{@"one": @"1",
                     @"null": [NSNull null],
                     @"two": @"2",
                     @"three": @"3"},
                   @"4"];
NSMutableArray *compactArray = [array cu_compactArray];
//=> ["0", "1", "2", {"one": "1", "two": "2", "three": "3"}, "4"]


動作の仕組みは、まず与えられたNSArrayかNSDictionaryの最初の階層についてだけ、NSNullをチェックして取り除きます。
ここで、チェックされるのはあくまでもネストした最初の階層についてだけです。

- (instancetype)initWithObjects:(const id [])objects count:(NSUInteger)cnt
{
    self = [super init];
    if (self) {
        NSNull *nul = [NSNull null];
        NSMutableArray *mutableArray = [[NSMutableArray alloc] initWithCapacity:cnt];
        for (NSUInteger i = 0; i < cnt; i++) {
            id object = objects[i];
            if (object && object != nul) {
                [mutableArray addObject:object];
            }
        }
        _original = mutableArray;
        
    }
    return self;
}


そして、元のNSArrayかNSDictionaryを保持して同様の振る舞いをしつつ、実際にネストしたNSArray/NSDictionaryにアクセスがあったときに、適宜同じようにNSNullを削除したラッパーを返すことで、最終的にすべてのネストされた要素からNSNullが取り除かれるという結果になります。

#pragma mark - primitive instance methods

- (NSUInteger)count
{
    return self.original.count;
}
- (id)objectAtIndex:(NSUInteger)index
{
    id object = self.original[index];
    if ([object isKindOfClass:[CUCompactArray class]] || [object isKindOfClass:[CUCompactDictionary class]]) {
        return object;
    }
    if ([object isKindOfClass:[NSArray class]]) {
        return [CUCompactArray arrayWithArray:object];
    }
    if ([object isKindOfClass:[NSDictionary class]]) {
        return [CUCompactDictionary dictionaryWithDictionary:object];
    }
    return object;
}


このようにすることで、最初にすべての要素をチェックする方法だとNSNullがたとえ1つも含まれていなかったり、その値にアクセスすることがなくても、同じだけ処理時間がかかっていたのですが、実際にアクセスのあったタイミングで必要なところだけ処理することで、パフォーマンスに対するペナルティは非常に小さくなります。

けっこうおもしろいアイデアかなと思っているのですがいかがでしょうか?

NSDateFormatterのパフォーマンスの話 #potatotips

クックパッド主催の第4回potatotipsでiOSのtipsとして日付のフォーマットをするときのパフォーマンスの話をしました。

きっかけ

きっかけは何気なくgistを眺めていたときに見つけたこれです。 ↓
Compare the date parsing performance of NSDateFormatter against SQLite's date parser for parsing an iOS 8601 date.

NSDateFormatter took  108.163 seconds
strptime_l took        21.656 seconds
sqlite3 took            7.096 seconds


そうそう、NSDateFomatterはけっこう遅いからフォーマットが固定だったりロケール関係ないときはstrptime_l使うよね、アップルのドキュメントにも載ってるし……

For date and times in a fixed, unlocalized format, that are always guaranteed to use the same calendar, it may sometimes be easier and more efficient to use the standard C library functions strptime_l and strftime_l.

Consider Unix Functions for Fixed-Format, Unlocalized Dates


と軽くスルーするところだったんですが、ちょっと気になる結果がありました。

sqlite3 took 7.096 seconds

sqlite3ってなんや!? しかも超速い!?

計測結果

ホンマかいな、ということで気になったので実際に試してみました。
試したコードは記事の最後に載せています。
先のgistに載っている結果は、100万件の日付の変換で(おそらく)シミュレータで実行した結果だと思いますが、私は実際のデバイスでやってみました。
使用したデバイスは、iPhone 4です。なぜiPhone 4で試したかというと、iOS 7の動くもっとも遅いデバイスで、たいていの現場でベンチマークとして(iPhone 4でそこそこ動けばOKみたいな)使われてるであろうという理由です。
件数は10,000件です。(100万件だとすごい時間かかるので。)

あと、CFDateFormatterRefもついでに測りました。

String => Date
(1回目)
NSDateFormatter     3.61784  seconds
CFDateFormatterRef  3.30721  seconds
strptime_l          0.655574 seconds
sqlite3             0.385894 seconds

(2回目)
NSDateFormatter     3.45504  seconds
CFDateFormatterRef  3.13984  seconds
strptime_l          0.654872 seconds
sqlite3             0.396366 seconds

(3回目)
NSDateFormatter     3.90896  seconds
CFDateFormatterRef  3.29913  seconds
strptime_l          0.67286  seconds
sqlite3             0.402761 seconds


同様の結果になりました。
NSDateFormatterとCFDateFormatterRefが一番遅く、strptime_lがそれより5〜6倍くらい速くて、sqlite3はさらに速いです。


NSDateFormatterは他の2つより複雑なことができるとはいえ、ちょっと遅すぎるような気がします。
今回測ったAPIはほぼすべてコードが公開されている(NSDateFormatterは無いけどCFDateFormatterRefは公開されている)ので時間があれば読んでみようかなと思います。

CFDateFormatter.c
strptime.c
SQLite Download Page

実際の使い分けについて

さて、それでは実際のケースでは常にNSDateFormatterを避けるべきかというと、それほど神経質になる必要はないと思います。
よくある例としてUITableViewCellに日付を表示する例を考えます。


↑ このような構成の画面はよくあると思いますが、1つのセルに日付は1つか2つの場合がほとんど(作成日、更新日)で、内部的に別の日付を使っている(並び替えなど)などがあったとしても多くて3つか4つでしょう。
その場合、NSDateFormatterを使ったとしても、1つの日付にかかる時間はiPhone 4で0.0004秒程度なので、60fpsの1フレームの時間が0.017秒くらいと考えると、日付処理だけが原因でスクロールがカクカクしたりすることは無いでしょう。


それよりは、日付表示などは国や地域によっても異なりますし、最近は相対表示がよく使われたりするなど、変わりやすいところなので、変更しやすかったり、他の人が読みやすいという点を優先したほうがいいと思います。

NSDateFormatterを避けたほうが良い場合

では積極的にNSDateFormatterを避けたほうが良いのはどのような場合かというと、ネットワーク系や特定のAPIを処理するライブラリだと思います。

例えば、下記のTwitter APIを使用して取得した200件JSONだと、私の場合512個の日付が含まれていました。

GET https://api.twitter.com/1.1/statuses/user_timeline.json?count=200&include_entities=true

Twitter APIの日付は"Thu Feb 13 04:00:25 +0000 2014"という形式なので、先ほど計測したものとは形が違いますが、
NSDateFormatterで512件の日付を処理するのに0.36秒ほどかかりました。(ちなみに先ほどの計測で使用した形式の日付なら512件で約0.2秒)
strptime_lで0.06秒です。

普通は通信とともにサブスレッドに処理を逃がすので、UIが固まることはないですが、初期表示がだいぶ遅くなってしまうことになるので、こういった場合はstrptime_lなどを使っていったほうが良さそうです。
特にAPIから返ってくる日付フォーマットがコロコロ変わることは普通なく、タイムゾーンはたいていはUTC固定なので、フォーマットを決め打ちできるので適用しやすいです。


参考事例としてもう一つ、MKNetworkKitに来たPull Requestを紹介します。
Replaced `NSDateFormatter` with faster `strptime_l` and `strftime_l` and fixed OS X compile errors. by Bo98 · Pull Request #230 · MugunthKumar/MKNetworkKit · GitHub

NSDateFormatterの処理は遅いのでstrptime_lと、strftime_lを使うように変更する、という内容です。


このような日付の処理が定型的でかつほぼ毎回つかわれるようなライブラリを作る場合は、効果が大きいので検討する価値は大いにあります。

おまけ: iPhone 5sで実行した結果 (10,000件)

参考までに、iPhone 5sで実行したときの結果も載せておきます。
iPhone 4で3秒以上かかっている処理が1秒かからずに終わっています。
次元の違う速さですね。

String => Date
(1回目)
NSDateFormatter     0.588108  seconds
CFDateFormatterRef  0.525327  seconds
strptime_l          0.128674  seconds
sqlite3             0.0242668 seconds

(2回目)
NSDateFormatter     0.57438   seconds
CFDateFormatterRef  0.523905  seconds
strptime_l          0.124121  seconds
sqlite3             0.0244989 seconds

(3回目)
NSDateFormatter     0.572262  seconds
CFDateFormatterRef  0.518393  seconds
strptime_l          0.119203  seconds
sqlite3             0.0247271 seconds

計測に使用したコード

#import "ViewController.h"

#import <mach/mach_time.h>
#import <time.h>
#import <xlocale.h>
#import "sqlite3.h"

typedef int64_t timestamp;

NSUInteger randomNumberInRange(NSUInteger start, NSUInteger end);

// Create a sample date using the ISO-8601 format.
// 2013-04-23T16:29:05Z
NSString *generateSampleDateString();
NSDate *generateSampleDate();

// Create an array of <count> dates in the ISO-8601 format.
NSArray *generateSampleDateStrings(NSUInteger count);

// Parse all given dates using NSDateFormatter
void parseDatesUsingNSDateFormatter(NSArray *dates);
void formatDatesUsingNSDateFormatter(NSArray *dates);

// Parse all given dates using strptime_l
void parseDatesUsingStrptime(NSArray *dates);
void formatDatesUsingStrptime(NSArray *dates);

// Parse all given dates using SQLite's strftime function
void parseStringToDateUsingSQLite(NSArray *dates);
void formatStringToDateUsingSQLite(NSArray *dates);

NSDate *parseDate(NSString *str);

static NSDateFormatter *dateFormatter = nil;
static CFDateFormatterRef dateFormatterRef = NULL;

NSArray *generateSampleDates(NSUInteger count)
{
    NSMutableArray *dates = [NSMutableArray array];
    
    for (int i = 0; i < count; i++) {
        [dates addObject:generateSampleDate()];
    }
    
    return dates;
}

NSArray *generateSampleDateStrings(NSUInteger count)
{
    NSMutableArray *dates = [NSMutableArray array];
    
    for (int i = 0; i < count; i++) {
        [dates addObject:generateSampleDateString()];
    }
    
    return dates;
}

NSString *generateSampleDateString()
{
    NSUInteger year = randomNumberInRange(1980, 2013);
    NSUInteger month = randomNumberInRange(1, 12);
    NSUInteger date = randomNumberInRange(1, 28);
    NSUInteger hour = randomNumberInRange(0, 23);
    NSUInteger minute = randomNumberInRange(0, 59);
    NSUInteger second = randomNumberInRange(0, 59);
    
    NSString *dateString = [NSString stringWithFormat:@"%lu-%02lu-%02luT%02lu:%02lu:%02luZ",
                            (unsigned long)year,
                            (unsigned long)month,
                            (unsigned long)date,
                            (unsigned long)hour,
                            (unsigned long)minute,
                            (unsigned long)second
                            ];
    
    return dateString;
}

NSDate *generateSampleDate()
{
    return parseDate(generateSampleDateString());
}

#pragma mark -

void parseDatesUsingNSDateFormatter(NSArray *dates)
{
    if (dateFormatter == nil) {
        dateFormatter = [[NSDateFormatter alloc] init];
        [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss'Z'"];
        [dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
    }
    
    for (NSString *dateString in dates) {
        NSDate *date = [dateFormatter dateFromString:dateString];
    }
}

#pragma mark -

void parseDatesUsingCFDateFormatter(NSArray *dates)
{
    if (dateFormatterRef == NULL) {
        dateFormatterRef = CFDateFormatterCreate(NULL, NULL, kCFDateFormatterNoStyle, kCFDateFormatterNoStyle);
        CFDateFormatterSetFormat(dateFormatterRef, CFSTR("yyyy-MM-dd'T'HH:mm:ss'Z'"));
        CFTimeZoneRef timeZone = CFTimeZoneCreateWithTimeIntervalFromGMT(NULL, 0);
        CFDateFormatterSetProperty(dateFormatterRef, kCFDateFormatterTimeZone, timeZone);
    }
    
    for (NSString *dateString in dates) {
        CFDateRef date = CFDateFormatterCreateDateFromString(NULL, dateFormatterRef, (__bridge CFStringRef)dateString, NULL);
    }
}

#pragma mark -

void parseDatesUsingStrptime(NSArray *dates)
{
    struct tm  sometime;
    const char *formatString = "%Y-%m-%dT%H:%M:%SZ";
    for (NSString *dateString in dates) {
        strptime_l(dateString.UTF8String, formatString, &sometime, NULL);
        NSDate *date = [NSDate dateWithTimeIntervalSince1970: timegm(&sometime)];
    }
}

#pragma mark -

void parseDatesUsingSQLite(NSArray *dates)
{
    sqlite3 *db = NULL;
    sqlite3_open(":memory:", &db);
    
    sqlite3_stmt *statement = NULL;
    sqlite3_prepare_v2(db, "SELECT strftime('%s', ?);", -1, &statement, NULL);
    
    for (NSString *dateString in dates) {
        sqlite3_bind_text(statement, 1, dateString.UTF8String, -1, SQLITE_STATIC);
        sqlite3_step(statement);
        timestamp value = sqlite3_column_int64(statement, 0);
        NSDate *date = [NSDate dateWithTimeIntervalSince1970:value];
        
        sqlite3_clear_bindings(statement);
        sqlite3_reset(statement);
    }
}

#pragma mark -

NSDate *parseDate(NSString *str)
{
    struct tm  sometime;
    const char *formatString = "%Y-%m-%dT%H:%M:%SZ";
    
    strptime_l(str.UTF8String, formatString, &sometime, NULL);
    NSDate *date = [NSDate dateWithTimeIntervalSince1970: timegm(&sometime)];
    return date;
}

NSUInteger randomNumberInRange(NSUInteger start, NSUInteger end)
{
    NSUInteger span = end - start;
    return start + arc4random_uniform(span);
}

double MachTimeToSecs(uint64_t time)
{
    mach_timebase_info_data_t timebase;
    mach_timebase_info(&timebase);
    return (double)time * (double)timebase.numer / (double)timebase.denom / 1e9;
}

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    printf("%s\n", [[UIDevice currentDevice] name].UTF8String);
    
    const NSUInteger count = 10000;
    
    printf("%lu count\n", (unsigned long)count);
    
    NSArray *dates;
    
    uint64_t begin;
    uint64_t end;
    
    printf("String => Date\n");
    dates = generateSampleDateStrings(count);
    
    begin = mach_absolute_time();
    parseDatesUsingNSDateFormatter(dates);
    end = mach_absolute_time();
    
    printf("NSDateFormatter     %g seconds\n", MachTimeToSecs(end - begin));
    
    begin = mach_absolute_time();
    parseDatesUsingCFDateFormatter(dates);
    end = mach_absolute_time();
    
    printf("CFDateFormatterRef  %g seconds\n", MachTimeToSecs(end - begin));
    
    begin = mach_absolute_time();
    parseDatesUsingStrptime(dates);
    end = mach_absolute_time();
    
    printf("strptime_l          %g seconds\n", MachTimeToSecs(end - begin));
    
    begin = mach_absolute_time();
    parseDatesUsingSQLite(dates);
    end = mach_absolute_time();
    
    printf("sqlite3             %g seconds\n", MachTimeToSecs(end - begin));
    
}

@end

CocoaPodsで導入しているライブラリのライセンス表記を自動的に作成する

CocoaPodsを利用している場合は、PodsディレクトリにPods/Pods-acknowledgements.plistまたはPods/Pods-acknowledgements.markdownが自動的に作成されていますので、それを利用して使用しているライブラリのライセンス表記を自動化できます。


一番簡単なのは設定アプリに項目を設けて表示することです。アプリケーションに画面を追加するわけではないのでお手軽です。


CocoaPodsのWikiにも同様の方法が載っています。


ライセンス表記は使用しているライブラリの構成が変わったときにだけ更新されればいいので、PodFileのpost_installフックで作成されるようにします。
これで`pod install`あるいは`pod update`したタイミングで自動的に更新されます。

post_install do | installer |
  require 'fileutils'
  FileUtils.cp_r('Pods/Pods-acknowledgements.plist', 'Ubiregi2/Settings.bundle/Acknowledgements.plist', :remove_destination => true)
end


アプリケーションのほうにはあらかじめ、入れ物としてのSettings.bundleをプロジェクトに用意しておきます。
最低限、下記のような構成になっていればいいです。


Acknowledgements.plistはそのまま設定アプリで表示できるように作られているので、Child PaneでAcknowledgements.plistを呼び出す設定にしておくだけです。


iPadの設定アプリで実際に表示したものが下記になります。

ユビレジのiPadアプリのCI環境をJenkinsからTravis CIに移行したときのまとめ


こちらの記事について、最新のTravis CIの環境(2014/4/15)ではコード署名に失敗する問題があります。
その問題の修正については下記の記事にまとめました。
Travis CIでipaを作るときのCode Signが失敗するのを修正したメモ - 24/7 twenty-four seven


実際は完全に移行したわけではなくて、Travis CIの有料プラン(プライベートリポジトリが使える)のフリートライアルを試しているところなのですが、しばらくはTravis CIでCIを動かすことにしたので、そのときの設定などをまとめます。


もともとは社内のサーバでJenkinsをホストしていて、それがダメということは全然ないのですが、社内でサーバをメンテナンスするのも面倒だし、ビルドスクリプトとかをポータブルな状態にしておくのは手元でサクッと実行できたりいろいろ都合が良さそうだと思い、試しにやってみることにしました。


Travis CIを選んだのは個人のOSSで使っていて慣れてるからと、Xcodeのビルド環境をホストしてくれるCIサービスの選択肢はそれほどないからです。
あと昨日知ったのですがCloudBeesiOS/MacのCIをサポートしてるということなので、こちらも試してみようかなと考えています。


やりたいことは

  • Githubにプッシュしたらそのタイミングでビルド&テストを実行
  • テストが成功したらTestFlightとCrittercismにアップロード
  • Pull Requestに対してもテストをしたい

の3点です。


副産物としてApple Developer Centerから最新のProvisioning Profileを自動的にダウンロードして使用する仕組みを作ったので、デバイスを追加したときに合わせてCI環境のProvisioning Profileをメンテナンスしなくてよくなりました。
ただ、この問題はエンタープライズのDeveloper Program(¥24,800/年)を購入するほうがスマートに解決できます(iOS Developer Enterprise ProgramのProvisioning Profileはデバイスの制限がなくなるので)。

ビルドスクリプトなど

ビルドスクリプトはrakeで書きました。
↓ Rakefileの全体はこちら

↓ .travis.ymlはこちら


Rakefileの設計は下記を参考にしました。


TestFlightへのアップロードは下記を参考にしました。


Crittercismへのアップロードは下記を参考にしました。


Apple Developer CenterからProvisioning Profileをダウンロードするスクリプトは下記を利用しました。


長いので要点だけ解説します。

ユニットテストの実行

テストの実行は54行目からの下記の部分です。

task :test do |t|
  DESTINATIONS.each do |destination|
    options = {
      sdk: 'iphonesimulator',
      workspace: WORKSPACE_DIR,
      scheme: SCHEME,
      configuration: 'Debug',
      destination: destination
    }
    options = join_option(options: options, prefix: "-", seperator: " ")
    sh "xcodebuild #{options} test | xcpretty -tc" do |ok, res|
      fail unless ok
    end
  end
end


コマンドラインで

$ rake test

とすると、下記のようなコマンドが実行されます。
DESTINATIONSにはとりあえずすべてのOSバージョンが書いてあるので、6.0, 6.1, 7.0, 7.0 (64 bit)のそれぞれのiPadシミュレータでテストが実行されます。

xcodebuild -sdk "iphonesimulator" -workspace "/Users/travis/build/ubiregiinc/ubiregi-client/Ubiregi2.xcworkspace" -scheme "Ubiregi2-Release" -configuration "Debug" -destination "name=iPad,OS=6.0" test | xcpretty -tc

アプリケーションのビルド

テストが成功したらTestFlightにAdHoc版としてアップロードするので、アプリケーションをビルドしてipaファイルを作ります。
ビルドは70行目〜、ipaファイルを作る部分にあたるのは147行目からです。

テストの場合とほぼ同じですが、後の工程でビルドされたappパッケージからipaファイルを作ったり、dSYMファイルをzipにしたりするので、OBJROOTとSYMROOTを指定してビルド先のディレクトリを扱いやすくしています。

また、ここでコード署名をしておかないと、ipaを作るときにAdHoc用のProvisioning Profileを埋め込むことができなかったので、署名だけしています。
Provisioning Profileまで指定すると今度はTestFlightにアップロードするときに失敗したのでProvisioning Profileの指定は空にしています。このへんは何度か設定を変えながらやってみたらできたという感じで、よくわかりません。

desc "Build application"
task :build do |t|
  options = {
    sdk: SDK,
    workspace: WORKSPACE_DIR,
    scheme: SCHEME,
    configuration: CONFIGURATION
  }
  options = join_option(options: options, prefix: "-", seperator: " ")
  settings = {
    OBJROOT: BUILD_DIR,
    SYMROOT: BUILD_DIR,
    CODE_SIGN_IDENTITY: DEVELOPER_NAME,
    PROVISIONING_PROFILE: ""
  }
  settings = join_option(options: settings, prefix: "", seperator: "=")
  sh "xcodebuild #{options} #{settings} clean build | xcpretty -c"
end

desc "Create .ipa file"
task :archive do |t|
  archive
end


トライ&エラーの内容を整理すると、

  • CODE_SIGN_IDENTITYを指定してPROVISIONING_PROFILEを空にすると下記のエラーでビルドが失敗しました。
xcodebuild -sdk "iphoneos" -workspace "Ubiregi2.xcworkspace" -scheme "Ubiregi2-Release" -configuration "Release" CODE_SIGN_IDENTITY="iPhone Distribution: Ubiregi Inc. (Y7522692LT)" PROVISIONING_PROFILE="" clean build
Code Sign error: No matching codesigning identity found: No codesigning identities (i.e. certificate and private key pairs) matching “iPhone Distribution: Ubiregi Inc. (Y7522692LT)” were found.

CodeSign error: code signing is required for product type 'Application' in SDK 'iOS 7.0'
  • CODE_SIGN_IDENTITYとPROVISIONING_PROFILEを両方とも空にすると下記のエラーでビルドが失敗しました。
xcodebuild -sdk "iphoneos" -workspace "Ubiregi2.xcworkspace" -scheme "Ubiregi2-Release" -configuration "Release" CODE_SIGN_IDENTITY="" PROVISIONING_PROFILE="" clean build
CodeSign error: code signing is required for product type 'Application' in SDK 'iOS 7.0'
  • `CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO`と指定して(ビルド時にコード署名しない)、PackageApplicationコマンドにsignオプションとembedオプションを指定すると、ビルドは成功するが下記のエラーでipaファイルの作成が失敗しました。
xcodebuild -sdk "iphoneos" -workspace "Ubiregi2.xcworkspace" -scheme "Ubiregi2-Release" -configuration "Release" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO clean build  
xcrun -sdk iphoneos PackageApplication -v "./build/Release-iphoneos/Ubiregi2.app" -o "./build/ubiregiinc/ubiregi-client/build/Ubiregi2.ipa" -sign "iPhone Distribution: Ubiregi Inc. (Y7522692LT)" -embed "$HOME/Library/MobileDevice/Provisioning Profiles/Ubiregi AdHoc.mobileprovision"
error: Failed to read entitlements from '/var/folders/9x/jfhnrxj531v5427xj48tsdd00000gn/T/S6ueKkDErz/Payload/Ubiregi2.app'
  • `CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO`と指定して(ビルド時にコード署名しない)、PackageApplicationコマンドにembedオプションだけを指定すると、ipaファイルの作成は成功するが下記のエラーでTestFlightへのアップロードが失敗しました。
xcodebuild -sdk "iphoneos" -workspace "Ubiregi2.xcworkspace" -scheme "Ubiregi2-Release" -configuration "Release" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO clean build  
xcrun -sdk iphoneos PackageApplication -v "./build/Release-iphoneos/Ubiregi2.app" -o "./build/ubiregiinc/ubiregi-client/build/Ubiregi2.ipa" -embed "$HOME/Library/MobileDevice/Provisioning Profiles/Ubiregi AdHoc.mobileprovision"
'Invalid IPA: missing embedded provisioning profile. Are you sure this is an ad hoc?'


上記の結果から考えて、ビルド時にコード署名をして、アーカイブ時にProvisioning Profileを埋め込むという手順でうまくいきました。

ipaファイルの作成

ビルドされたappパッケージからipaファイルを作ります。
「アプリケーションのビルド」のところで述べたように、ここではTestFlightにアップロードするためのAdHoc用のProvisioning Profileを指定します。

def archive
  options = {
    o: IPA_FILE,
    embed: PROVISIONING_PROFILE
  }
  options = join_option(options: options, prefix: "-", seperator: " ")
  sh "xcrun -sdk #{SDK} PackageApplication #{APP_FILE} #{options}"
end


Provisioning Profileを埋め込むためには証明書と秘密鍵(cerファイルやp12ファイル)が必要なので、それらはキーチェーンからエクスポートして、あらかじめリポジトリに含めておきます。

apple.cerはキーチェーンに登録されている「Apple Worldwide Developer Relations Certification Authority」、dist.cerとdist.p12は今回はUbiregi Inc.の「iPhone Distribution」の証明書と秘密鍵です。


そして、上記のコマンドの前に、一時的なキーチェーンを作成してこれらの各種証明書を追加します。
また、Travis CIで実行する分には後始末は必要ないのですが、手元で実行するときには同じ名前のキーチェーンを作ろうとすると失敗するので、コマンドの実行後には先ほど作ったキーチェーンを削除します。

↓ こちらを参考にしました。
Deploy an iOS app to testflight using Travis CI

def add_certificates
  sh "security create-keychain -p travis #{KEYCHAIN_NAME}"
  sh "security import ./certificates/apple.cer -k ~/Library/Keychains/#{KEYCHAIN_NAME} -T /usr/bin/codesign"
  sh "security import ./certificates/dist.cer -k ~/Library/Keychains/#{KEYCHAIN_NAME} -T /usr/bin/codesign"
  sh "security import ./certificates/dist.p12 -k ~/Library/Keychains/#{KEYCHAIN_NAME} -P #{KEY_PASSWORD} -T /usr/bin/codesign"
  sh "mkdir -p \"#{PROFILE_DIR}\""
  sh "cp \"./profiles/#{PROFILE_NAME}.mobileprovision\" \"#{PROFILE_DIR}\""
end

def remove_certificates
  sh "security delete-keychain #{KEYCHAIN_NAME}"
end

TestFlightとCrittercismにアップロード

ipaファイルをTestFlightに、dSYMファイルをCrittercismにアップロードします。
112行目からのタスクです。

task :crittercism => [DSYM_ZIP_FILE] do |t|
  fields = {
    dsym: "@#{DSYM_ZIP_FILE}",
    key: API_KEY,
  }
  fields = join_option(options: fields, prefix: "-F ", seperator: "=")
  sh "curl -sL -w \"%{http_code} %{url_effective}\\n\" https://api.crittercism.com/api_beta/dsym/#{APP_ID} #{fields} -o /dev/null"
end

desc "Upload IPA file and dSYM file to TestFlight and notify testers"
task :testflight => ["version:set_build_version", IPA_FILE, :crittercism] do |t|
  if ENV['TRAVIS_PULL_REQUEST'] != "false"
    puts "This is a pull request. No deployment will be done."
    next
  end
  if ENV['TRAVIS_BRANCH'] != "master"
    puts "Testing on a branch other than master. No deployment will be done."
    next
  end

  release_date = DateTime.now.strftime("%Y/%m/%d %H:%M:%S")
  release_notes = "Build: #{InfoPlist.marketing_and_build_version}\nUploaded: #{release_date}"

  fields = {
    file: "@#{IPA_FILE}",
    api_token: API_TOKEN,
    team_token: TEAM_TOKEN,
    notes: release_notes,
    notify: true,
    distribution_lists: DISTRIBUTION_LISTS
  }
  fields = join_option(options: fields, prefix: "-F ", seperator: "=")
  sh "curl -sL -w \"%{http_code} %{url_effective}\\n\" http://testflightapp.com/api/builds.json #{fields} -o /dev/null"
end


と、こんな感じでGithubにプッシュすると自動的にテストが実行されて、(masterブランチへのPushなら)TestFlightやCrittercismへのアップロードもやってくれるようになりました。

ビルドスクリプトはそれぞれrakeのタスクとして作ってあるので、手元でも簡単に

rake test

とテストを実行したり、

rake testflight

と簡単にTestFlightにアップロードすることができるようになりました。

注意点

Travis CIのデフォルトのOS Xの環境ではなぜか`pod install`がSegmentation faultで失敗しました。

$ pod install

Analyzing dependencies

/Users/travis/.rvm/gems/ruby-2.0.0-p247/gems/xcodeproj-0.14.1/ext/xcodeproj/xcodeproj_ext.bundle: [BUG] Segmentation fault

ruby 1.8.7 (2012-02-08 patchlevel 358) [universal-darwin12.0]


同様のエラー報告がIssueに登録されていて対処法はとりあえず`rvm: 1.9.3`としてrubyのバージョンを1.9.3に固定するとのことでした。
iOS - Pod Install Erroring Out · Issue #1657 · travis-ci/travis-ci · GitHub


1.9.3だとキーワード引数が使えないとかrakeを書くときにちょっと制限ができちゃいますが、とりあえずはそうしました。早く修正されるといいですね。

おまけ1:Provisioning Profileの自動ダウンロード

Jenkinsを使っているときからもあった問題に、Provisioning Profileのメンテナンスがあります。
デバイスを追加があったときに、CIのマシンが参照しているProvisioning Profile(リポジトリに含めるなどする)も更新する必要があるからです。

今回はそれも何とかしたいと思って、CIの実行時に毎回最新のものをダウンロードするようにしました。
177行目からのタスクになります。

namespace :profile do
  desc "Download provisioning profiles from Apple Developer Center"
  task :download do
    ruby "./scripts/apple_dev_center.rb -C ./scripts/apple_dev_center.config -d profiles -O /dev/null"
  end
end


下記のスクリプトを実行すると、AppleのWebサイトに接続して、Provisioning Profileをダウンロードしてprofilesディレクトリに保存します。
ipaファイルを作るときにはこのProvisioning Profileが使用されます。

ruby "./scripts/apple_dev_center.rb -C ./scripts/apple_dev_center.config -d profiles -O /dev/null"


Apple Developer CenterからProvisioning Profileをダウンロードするスクリプトは下記を利用しました。
lacostej/apple-dev · GitHub


上記のスクリプトはgemの体裁は整っているのですが、RubyGemsとしては登録されておらず、READMEに載っている使い方もbinのスクリプトを直接利用するように書いてあったので、gemとしてインストールしても使い方がよくわからなかったので、スクリプトごとリポジトリに含めて利用するようにしました。

おまけ2:バージョン番号とビルド番号の設定

TestFlightにアップロードするときにどのビルドかを区別するためにgitのハッシュがビルド番号に設定されるようにします。
257行目です。

desc "Sets build version to last git commit (happens on each build)"
task :set_build_version do
  rev = `git rev-parse --short HEAD`.strip
  puts "Setting build version to: #{rev}"
  InfoPlist.build_version = rev
end

ユビレジではバージョン番号のポリシーとして、「X.XX」という形式で、マイナー番号が偶数がリリース版、奇数ならベータ版という決まりにしているので、申請したらとりあえずマイナーを1つ上げるなどの操作をよくするので、ついでに手元でバージョン番号を簡単に操作できるようにしました。

例えば現在のバージョンが2.56とすると、

rake version:bump:minor

とすると、2.57になります。

rake version:bump:release

とすると、現在のバージョンが2.56でも2.57でも2.58になります。

このとき、ビルド番号はgitのハッシュではなくて、番号っぽくコミット数になるようにしています。
アプリ内にバージョンを表示しているのでこちらのほうが見栄えがなんとなくいいかなと思ってそうしています。

下記を参考にしました。
How to Automatically Update Xcode Build Numbers from Git | Objective C#

Conference with DevelopersでJavaScriptCore.frameworkとObjective-C Runtime APIについて話しました

年に1度のiOSデベロッパーのイベント「Conference with Developers」で話をしました。
JavaScriptCore.frameworkとObjective-C のRuntime APIという非常にマニアックな内容でしたが、まあまあわかるように伝えられたかなと思います。


話の内容は主に以下の3点です。

  • JavaScriptCore.frameworkの概要と使い方
  • Objective-C Runtime APIの活用方法
  • JavaScriptBridgeの紹介


伝えきれなかったことを補足しますと、JavaScriptBridgeはフルスクラッチで最初から最後まですべてJavaScriptでiOSアプリを書く、という用途のために作られたのではありません。


例としてそういうものを示しているのは、単に例は極端なほうがわかりやすいというだけの理由です。


どちらかというと、週ごとに変わるキャンペーン用画面とか、メンテナンス時に一時的に表示される画面など、単純に変更が頻繁なところや、一時的に表示する画面だけど様々なケースが考えられる、というところに部分的に利用するのが適していると思います。


(コンパイル時ではなく)実行時に文字列を評価して実行されるという強力な特性を「気軽に」利用できるという点が大きなメリットです。


というのも、JavaScriptCore.frameworkでJSからObjective-Cを扱う場合は、JSExportというJSに公開するインターフェースをクラスごとに準備する必要があるのと、delegateメソッドやUIViewControllerのviewDidLoadなどのフレームワークから呼ばれるメソッドに応答することができないという制限があるので、通常はコストメリットが割に合わないためです。
(使うクラスすべてのJSExportを用意するのは非常に面倒、delegateその他に応答できないとUIKitをまともに利用できない)


JavaScriptBridgeにはiOS SDKに含まれるほぼすべてのクラスに対してJSExportプロトコルが準備されていて、Runtime APIで適宜JSのメソッドにディスパッチすることでdelegateメソッドへの応答を可能にしているので、このライブラリを導入するだけであとはおもむろにJSで書き始めることができるようになります。


この手軽さこそが唯一最大の利点であると考えています。


あと、懇親会で話してるときに、子ども向けのプログラミング環境をiPadで提供する、というのはとても夢があって実用的な例だと思いました。
(やろうと思えば)Twitterクライアントくらいは作れちゃうわけですし、書いたそばから実行して動かせるというのはとても楽しいし、そもそも現状iOSでiOS Appを書ける環境はほぼないので普通に便利、と実現すればとてもおもしろい試みじゃないかなと思います。

資料

ビデオ(前半)



Video streaming by Ustream

ビデオ(後半)



Video streaming by Ustream

コマンドライン引数(Launch arguments)は思ったより簡単に使える



iOS/AndroidのTips共有会potatotipsでiOSの実行時引数は思ってるより簡単ベンリに扱えるんだよって話をしました。


potatotips (iOS/Android開発Tips共有会) 第3回



↑ 起動時のオプション引数はiOSだと上図のようにXcodeのスキーマで指定します。

int main(int argc, char * argv[])

↑ そして普通のUnixプログラムと同様にmain関数のargcに引数の個数が、argvに文字列の配列で入ってきます。
でもGUIの無いコマンドラインのユーティリティプログラムならともかく、iOSアプリでargvをParseして何かするとか面倒なだけだと思っていませんか?


詳しいことはスライドに書いたので端的に言うと、ある規則にしたがうとこの引数は自動的にParseされてNSUserDefaultsに格納されます。
つまりアプリケーションのどこでも特定のキーで値を取得することができるようになります。

ある規則とは次の2つです。

  • キーとなる値は`-`(ハイフン)で始まる
  • 2語以上の値はクオートで囲む
-key1 value1 -key2 'foo bar'

↑ 例えば上のように書くと、NSUserDefaultsに"key1", "key2"にそれぞれ"value1", "foo bar"という値が入ります。


また、この挙動を利用して既存の値を一時的に上書きすることもできます。

-AppleLanguages (en)

↑ 上記のように起動時引数を指定すると一時的に英語環境で起動されるのはよく知られたデバッグのTipsですが、これはNSUserDefaultsの`AppleLanguages`というキーの値を上書きしているのでそのような挙動になります。


起動時引数の値はNSUserDefaultsに永続化されるわけではありません。あくまでも起動してから終了するまでの一時的なものです。
そのような挙動はNSUserDefaultsの値が「ドメイン」という階層を持っていて、起動時引数はNSArgumentDomainという階層に格納され、その階層が一番最初に検索されるので、同じキーがあれば最初にヒットするので結果的に上書きされたことになるという仕組みです。


そして、意外に知られていないと思われるのが、ArrayやDictionaryのデータ構造を指定できるということです。


例えばArrayは次のようになります。

-arrayArg '( "foo", "bar", "baz" )'


Dictionaryはこうです。

-dictArg '{ "foo" = "bar"; "baz" = "qux"; }'


記法でわかるとおり、プロパティリストの表現形式です。
NSUserDefaultsの永続化先はプロパティリストなので納得ですね。


ArrayとDictionaryの組み合わせで、もっと複雑なデータを渡すこともできます(だいぶややこしくなりますが)。

上記のプロパティリストのフォーマットは古い記法ですが、今どきのXML形式も使えます。

-xmlArg “<dict><key>foo</key><string>bar</string><key>baz</key><string>qux</string></dict>"


起動時のタイミングでXMLのParseがなされているとか、それは本当に必要なのかという気がしますがとにかくそういう仕組みです。


これで、なぜ下のように書くと英語環境になるのかということのトリックがわかります。

-AppleLanguages (en)

第一引数の`-AppleLanguages`はハイフンで始まっているのでNSUserDefaultsにAppleLanguagesというキーで格納されます。
第二引数の`(en)`はプロパティリストの配列です。


NSUserDefaultsにはあらかじめAppleLanguagesというキーで設定で指定した優先順位で言語の配列が格納されています。

NSLog(@"%@", [[NSUserDefaults standardUserDefaults] dictionaryRepresentation]);

=>
{
    ...

    AppleLanguages =     (
        ja,
        en,
        fr,
        ...
    );

    ...

}


その値を上書きするために第一引数はハイフンで始まる必要があり、第二引数にカッコを付けるのは、もともとのAppleLanguagesの値が配列なので配列を渡す必要があり、配列のプロパティリストでの表現形式はカッコで囲む必要があるからです。

One-line fix for UITextView on iOS 7

iOS 7のUITextViewのバグを1行で直す裏ワザ

[[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"UIDisableLegacyTextView"];

解説

iOS 7にはバグだらけのUITextViewのほかに_UICompatibilityTextViewというiOS 6以前のUITextView(と思われる)クラスがあり、
メモ(Notes.app)など同様の不具合が再現しない標準アプリは_UICompatibilityTextViewが使われている。


そしておそらくこのフラグによってUITextViewをインスタンス化したときにどちらが使用されるかが変わる。


以下のようにmain.mあたりで設定すると有効になる。

#import <UIKit/UIKit.h>
 
#import "AppDelegate.h"
 
int main(int argc, char * argv[])
{
    @autoreleasepool {
        [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"UIDisableLegacyTextView"];
        
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

JavaScriptでiOSアプリが書けるライブラリJavaScriptBridgeを公開しました

kishikawakatsumi/JavaScriptBridge · GitHub


前にiOS 7から導入されたJavaScriptCore.frameworkを使ってUIKit標準のクラスを操作する話を書きました。

JavaScriptだけでiOSのUIを書いてみる - 24/7 twenty-four seven


JavaScriptCoreを使ってJavaScriptからObjective-Cのオブジェクトを操作するのは、あらかじめJSExportプロトコルで公開しておく必要があるなど、あまり実用的ではありませんでしたが、このライブラリを導入すうることで、そういった事前準備がすでに済んでいる状態で使いはじめることができます。


また、JavaScriptからObjective-Cのクラスを定義できるなどいくつかの拡張を加えてあり、UIViewControllerのサブクラスで画面を定義したり、デリゲートに応答するオブジェクトを作ったり、UIKitの作法にしたがってアプリケーションを記述することができるようになっています。


使い方を見てもらえればわかりますが、全部JavaScriptで書くことを強制されることはなく、部分的に使用することもできます。
むしろ、ピンポイントで一部の画面だけJavaScriptを使用して書きたい、という場合に力を発揮するのではないかと思います。

応用の方法としては、キャンペーンや期間限定の画面などを増減できるようにしたり、変更可能にしておくといった方法や、プロトタイピングで、再ビルドやアプリケーションを終了することなく、動的に修正を確認したりなどに向いているかと考えています。


まだバージョン0.0.1のベータ版ですが、バグ報告、プルリクエストをお待ちしています。

使い方

もっとも簡単な例は下記になります。

#import <JavaScriptBridge/JavaScriptBridge.h>
...

// Retrieve the prepared context
JSContext *context = [JSBScriptingSupport globalContext];

// Add framework support if needed.
// ('Foundation', 'UIKit', 'QuartzCore' enabled by default.)
[context addScriptingSupport:@"MapKit"];
[context addScriptingSupport:@"MessageUI"];

// Evaluate script
[context evaluateScript:
 @"var window = UIWindow.new();"
 @"window.frame = UIScreen.mainScreen().bounds;"
 @"window.backgroundColor = UIColor.whiteColor();"
 @"window.makeKeyAndVisible();"
];


JSBScriptingSupportから取得したJSContextオブジェクトは標準のUIKitやFoundationのほとんどのクラス群がすでにJSExportプロトコルに適合している状態になっています。

JSContext *context = [JSBScriptingSupport globalContext];


必要に応じてJSExportに適合させるクラスを追加します。
Frameworkごとに名前を指定することでそのFrameworkに属するクラス群がJSExportに適合されます。
例えば下記のようにMapKitを指定すると、MKMapViewなどがJavaScriptから扱えるようになります。

[context addScriptingSupport:@"MapKit"];


必要なことはこれだけです。あとはおもむろにJavaScriptでアプリケーションを書き始めることができます。

// Evaluate script
[context evaluateScript:
 @"var window = UIWindow.new();"
 @"window.frame = UIScreen.mainScreen().bounds;"
 @"window.backgroundColor = UIColor.whiteColor();"
 @"window.makeKeyAndVisible();"
];

文法など

変数宣言

型名を取り除きます。

UILabel *label;
var label;
プロパティ

プロパティはほぼObjective-Cと同様でドットを使います。
Objective-Cではメソッド呼び出しの形式でプロパティにアクセスすることができますが、メソッドによるアクセスは今のところサポートしていません。

UISlider *slider = [[UISlider alloc] initWithFrame:frame];
slider.backgroundColor = [UIColor clearColor];
slider.minimumValue = 0.0;
slider.maximumValue = 100.0;
slider.continuous = YES;
slider.value = 50.0;
var slider = UISlider.alloc().initWithFrame(frame);
slider.backgroundColor = UIColor.clearColor();
slider.minimumValue = 0.0;
slider.maximumValue = 100.0;
slider.continuous = true;
slider.value = 50.0;
メソッド呼び出し

Objective-Cのカッコを使ったメソッド呼び出しは、ドット記法に置き換えます。
パラメータを取るメソッドについては、コロンを取り除き、パラメータのラベルをキャメルケースで連結してメソッド名にします。
そのあと、順番にパラメータを並べます。

UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
var window = UIWindow.alloc().initWithFrame(UIScreen.mainScreen().bounds);
クラス定義

次のように、JSB.defineメソッドを使うとObjective-Cのクラスが作成されます。
第1引数でクラスの宣言、第2引数にハッシュでメソッド名と処理のfunctionオブジェクトを渡します。
これを使うことでUITableViewのデリゲートメソッドに応答したり、UIViewControllerのサブクラスとして画面を作成したりできます。

var MainViewController = JSB.define('MainViewController : UITableViewController <UITableviewDataSource, UITableviewDelegate>', // Declaration
// Instance Method Definitions
{
  viewDidLoad: function() {
    self.navigationItem.title = 'UICatalog';
  },
  tableViewNumberOfRowsInSection: function(tableView, section) {
    return self.menuList.length;
  },
  tableViewCellForRowAtIndexPath: function(tableView, indexPath) {
    var cell = UITableViewCell.alloc().initWithStyleReuseIdentifier(3, 'Cell');
    cell.accessoryType = 1;
    cell.textLabel.text = self.menuList[indexPath.row]['title'];
    cell.detailTextLabel.text = self.menuList[indexPath.row]['explanation'];

    return cell;
  },
  tableViewDidSelectRowAtIndexPath: function(tableView, indexPath) {
    var targetViewController = self.menuList[indexPath.row]['viewController'];
    self.navigationController.pushViewControllerAnimated(targetViewController, true);
  }
});
モジュール

Node.js風の簡単なモジュールシステムを提供しています。
JSB.require('/path/to/module.js') と書くと別のファイルのモジュールが利用可能になります。
モジュールとして公開するにはJSB.exportsにオブジェクトを代入します。

ファイルを分けて書けるのでちょっと大きめのアプリケーションでもJavaScriptだけでけっこう書けます。
↓ このサンプルを見るとよくわかります。
JavaScriptBridge/Examples/UICatalog/UICatalog/js at master · kishikawakatsumi/JavaScriptBridge · GitHub

var ButtonsViewController = JSB.require('buttonsViewController');
var ControlsViewController = JSB.require('controlsViewController');
var WebViewController = JSB.require('webViewController');
var MapViewController = JSB.require('mapViewController');

var MainViewController = JSB.define('MainViewController : UITableViewController', {
  viewDidLoad: function() {
    self.navigationItem.title = 'UICatalog';
    ...
});

JSB.exports = MainViewController;