意識高いiOSアプリのつくり方

基本編

1. Objective-Cで書く

Obj-C使いたくないが諦める

結局Obj-C使うのが一番楽であることに気づくのだ

2. Xcodeを使う

VimとかEmacsとかAppCodeで書きたいが諦める

結局Xcode使うのが一番楽であることに気づくのだ

設計編

3. 仕様とUIをしっかり設計してから実装する

きちんと設計しないとあとから大量の手戻りが発生して泣きたくなる。

  • 技術的に可能なことをやろうとしているのか
  • 基本的な画面設計
    • メインのビュー部分
    • ナビゲーション方法(TabBarなのかNavigationBarなのかNavigationDrawerなのか)

くらいは最低限調査・設計しておく

4. フレームワーク的ライブラリを使わない
  • UIKitは既に十分にフレームワークになっており、新たな思想を持ち込む必要はない
  • Reactive Cocoaを使うのはUIKitのMVCをよくよく理解してからでも遅くない
  • BlocksKitはbad parts. 古典的delegateを使用したほうが見通しもメモリ管理も良くなるケースのほうが多い
5. なるべくWebViewは使わないようにする
  • デバッグが難しくなる
  • Obj-Cとのブリッジ部分がバグの温床になりやすい
  • 使う場合はデータのやりとりは最小限に

クラス設計編

6. ViewControllerには最低限の責任しか持たせない。ModelとカスタムViewを積極的につくる

こんなViewControllerはクソの山だ!

  • 1000行超えてる
  • JSONをパースしている
  • JSONをパースしただけの生dictionaryを直接読み取り・操作している
  • subViewの見た目(colorとかframeとか)を操作している
  • 別のviewControllerと密結合してdisappearなときにも仕事をしている

続き:あとでかく

引っ越した #tqhouse

卓球ハウスというシェアハウスを作って引越しました。


最近引きこもりが加速して外出するのもダルくなりました。


家でイベントを開けば外出しなくてもいいことに気づいたのでイベントできるようにリビングが広めの一軒家を借りました。


さっそく、今週末は「ミルキィハッカソン」をやります。


ミルキィホームズを見ながらハックするナイスイベントになる予定です。


こういうひどいイベントどんどんやって行きたい

なんとなくRSpec使ってるやつダサい

先日、第5回若手Webエンジニア交流会でLTしてきました。
ビール飲んでピザつまみながらLTきいたりLTしたりするのカジュアルでいいですね。


というわけで、スライド公開します。
Testing Frameworkについてで、
「なんとなく、とりあえず、RSpec」みたいなのはダサいよねというお話

スライドの原稿

テストとかの話

矢口裕也
(@yayugu)

「みんなー
テスト書いてる?」

みたいなの飽きたよね

今日は
TDDとBDDとAgileとかの開発スタイル
と
テスト哲学の話を

しません

Testing Framework
の話をします

Testing Framework

大きく分けて2つ

xUnit
xSpec

xUnit

class TestInteger < UnitTest
  def test_add
    assert_equal(2, 1 + 1)
  end
end

xSpec

describe Integer
  context 'when add 2 numbers' do
    it 'should return sum' do
      2.should eq(1 + 1)
      expect(2).to eq(1 + 1)
    end
  end
end

ここで質問

どっちが好き?

xUnit

xSpec

ほうほう
なんで?

自分がなぜ
そのテストフレームワークを
好きか

答えられる?

みんなに言いたい

自分が使う
ツールのことを
もっと良く知ろう

フレームワークは
よく考えて
ちゃんと選ぼう

自分の話

さいきんRubyだと
minitest使ってる

iOSだと
SenTestingKit

何を考えて
これらを選んだのか

xUnit
xSpec

この2つを比較

※思想とかは考えない

比較方法

機能でそれぞれ2つの
パーツに分類してから比較

xUnit
 - test class
  class TestHoge, def test_hoge
 - assertion
  assert_equalとか

xSpec
 - behavior
  describe, context, it
 - expectation
  should, expect

(1)機能の違い
(2)表記の違い
(3)実装の違い

(1)機能の違い

assertionとexpectation
→同じもの

test classとbehavior
→振る舞いの階層化ができるか

(2)表記の違い

assertionと
expectationに
ついてのみ比較

参考フレームワーク:
Ruby
 - minitest
 - RSpec
Obj-C
 - SenTestingKit
 - Kiwi

Ruby

minitest

 assert_equal(2, 1 + 1)

RSpec
(should)

 (1 + 1).should eq(2)

RSpec
(expect)

 expect(1 + 1).to eq(2)

 assert_equal(2, 1 + 1)
 (1 + 1).should eq(2)
 expect(1 + 1).to eq(2)

Obj-C

SenTestingKit

 STAssertEquals(2, 1 + 1);

Kiwi
(should)

 [[foo should] equal:bar];

Kiwi
(expect)

[[theValue(1 + 1) should]
  equal:theValue(2)];

 STAssertEquals(2, 1 + 1)
 [[foo should] equal:bar];
 [[theValue(1 + 1) should] equal:theValue(2)];

表記の簡潔さ

assert > should > expect

(3)実装の違い

assert
の実装

global or
test classのインスタンスに
assert関数を定義

class UnitTest
  def assert_equal(expect, actual)
    if (expect == actual)
      puts '.'
    else
      puts('F')
    end
  end
end

expectation
の実装

2スタイルある
- should
- expect

should
の実装

RSpec

BasicObject.module_eval do
  def should(...)
    ...
  end
end

BasicObjectに
shouldを追加!!!

voodoo感ある

Kiwi

@interface NSObject (...)
- (void)should;
...
@end

NSObjectに
カテゴリとして
shouldを追加!!!

voodoo感ある

expect
の実装

RSpec

::RSpec::Matchers.module_eval do
  def should(...)
    ...
  end
end

BasicObjectオブジェクトを
汚染しないため
shouldよりmagic少ない

toやeqの実装は
面倒そう

Kiwi

#define theValue(expr) \
({ \
    ...
    [KWValue valueWithBytes:...
})

Kiwiも
同様

実装の健全さ

assert > expect > should

まとめ

-test class, behaviorの機能差
 階層化できるか
-assert, expectの機能差
  なし
-表記の簡潔さ
 assert > should > expect
-実装の健全さ
 assert > expect > should

使うべき
Testing Framework

A. 階層化が不要:
test class + assertion
→ xUnit

B. 必要:
階層構造 + assertion
→

minitest
Test::More subtest
など

を使うべき

サンプルコード

describe Integer
  context 'when add 2 numbers' do
    it 'should return sum' do
      assert_equal(2, 1 + 1)
    end
  end
end

ここに書いたのは
あくまで俺の考え

「なんとなく」
ではなくちゃんと考えて
Testing Framework
を選んで使おう

Happy testing!

技術系イベントの定員について

あっという間に埋まってしまうことが多いですが、補欠でもとりあえず申し込んでおきましょう

  • 意外と参加できなくなる人は多いので順番が回ってくる
  • 補欠が多いと開催者が定員を増やせるようがんばってくれる

余談1

イベントによっては盛り上がりを演出するため、最初は定員を少なめにしておいて補欠が出たあたりから枠を拡大していく手法をとることがあります。

余談2

イベント運営者は参加申請した人の50%くらいしかこないケースを覚悟しておきましょう。リマインドメールを3回くらい送ってもそれしか来ないと泣きなくなりますが世の中そんなもん

PHPすげえ

もともとRubyistだったんですが1年程仕事でPHPを書いて意外とイケてることに気づいたので。

  • 運用が楽
  • 速い
  • 開発環境がわりと快適
  • ドキュメントが高品質

運用が楽

Apacheにmod_phpを入れてindex.phpを叩けばそれだけでスタートできます。
1リクエスト1プロセスで処理され、1リクエストを返し終わったプロセスは破棄されます
Rubyだと基本的にプロセスを複数リクエストに使いまわすため、プロセスが肥大化したり、リクエストの処理中に大きなGCが走って遅くなったり、それを防ぐために一定回数で再起動したり、プロセスを監視するためのプロセスが必要になったりと複雑になりがちですが、そういった手間はありません。
デプロイしたあとは非常に安定して動き続けてくれます。

速い

1リクエストごとにプロセスを立ち上げると、とても遅そうですが(Railsで同じ事をするとどうなるか想像してみよう)
PHPは高速に動作します。

開発環境がわりと快適

ソースコードの変更も即座に反映されます。これにより「Webrickおせー」「Shotgunおせー」「touch tmp/restart.txtだりー」「guardの更新検知がたまに若干遅れるんだけど」といったことから開放されます。

ドキュメントが高品質

php.netにほぼすべての関数、クラスについて丁寧で最新の変更に追随している英語&日本語ドキュメントが用意されています。
罠っぽい仕様やその回避策もひと通り網羅されていて、困ったらマニュアル読むだけでなんとかなります。


イケてないところは多すぎるので

結論

つくったあと放っとけば動き続けてくれて、保守を他の人にぶん投げられるサービスがつくりたければPHPを使え。

PhoneGap/CordovaがiOSのネイティブ機能を使うしくみ

さいきんWebとiOS/Andoroid Native Appの融合がすすんでいますね!

ブラウザコンポーネント(WebViewという)のJSからiOSのネイティブ機能を呼び出すスタイルも流行っています. PhoneGapとかFacebookアプリとかね!


自分は勝手にJS-Native Bridgeと呼んでいるのですが,
あれってどうやってるのって話


Google先生で「uiwebview native」でぐぐる
UIWebViewでWebとネイティブを相互連携させる方法について
iPhoneのネイティブ機能をWebViewから呼び出す方法

こういう記事が出てきます
ようするに

Obj-C → JS [webView stringByEvaluatingJavaScriptFromString:@"JSCode"]
JS → Obj-C webView:shouldStartLoadWithRequest:navigationType: を発火させる

だが甘い

↑の記事ではあくまで原理の解説にしかなっていません.
現実のコードで使うにはもっと改良が必要なのです.


具体的には:

  • iframe fire
  • big size data support
  • queuing

iframe fire

で,イベントを発火させる方法として↑ではaタグをクリックさせたり,location.hrefをいじっていますが,そんなことしなくてもiframeを作ってやると発火します.こっちのほうが影響範囲が少なくクリーンです.


https://github.com/apache/incubator-cordova-js/blob/master/lib/ios/exec.js

gapBridge = document.createElement("iframe");
gapBridge.setAttribute("style", "display:none;");
gapBridge.setAttribute("height","0px");
gapBridge.setAttribute("width","0px");
gapBridge.setAttribute("frameborder","0");
document.documentElement.appendChild(gapBridge);
gapBridge.src = "gap://ready";

これでイベントが発生してくれます

big size data support

URLでもある程度の情報は伝えられるのですが,あまり大きなものはダメです.
特に画像とか.そのためデータはURIに付加するのではなくJSON化してstringByEvaluatingJavaScriptFromString経由で取得するようにします.
↓の擬似コードみたいなイメージ

js

cordova.command = JSON.stringify(commandArgs);
createIframe();

Obj-C

- (BOOL) webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType
{
  NSURL* url = [request URL];
  if ([[url scheme] isEqualToString:@"gap"]) {
    NSString *json = [webView stringByEvaluatingJavaScriptFromString:@"cordova.command"];
    NSDictionary *params = [NSJSONSerialization dataWithJSONObject:json options:nil error:nil]
    [self dosomething] // なんかする
    return NO;
  }
}

queuing

webView:shouldStartLoadWithRequest:はある一定時間内に複数回発生させようとしても1回しか発生しません.おそらくJSのrun loopに戻れるまでイベントが発生させられないことなどが関係しているような.

cordova.command = JSON.stringify(commandArgs) // 無視される
createIframe();
cordova.command = JSON.stringify(commandArgs2)
createIframe();

そのためcommandのqueuingが必要となります.

cordovaではcommandをcommandQueueという配列に入れ,commandQueueFlushingというフラグで管理することで対処しています.


https://github.com/apache/incubator-cordova-js/blob/master/lib/ios/exec.js

if (cordova.commandQueue.length == 1 && !cordova.commandQueueFlushing) {
    if (!gapBridge) {
        createGapBridge();
    }
    gapBridge.src = "gap://ready";
}

https://github.com/apache/incubator-cordova-ios/blob/master/CordovaLib/Classes/CDVViewController.m

- (void) flushCommandQueue
{
    [self.webView stringByEvaluatingJavaScriptFromString:@"cordova.commandQueueFlushing = true"];

    // Keep executing the command queue until no commands get executed.
    // This ensures that commands that are queued while executing other
    // commands are executed as well.
    int numExecutedCommands = 0;
    do {
        numExecutedCommands = [self executeQueuedCommands];
    } while (numExecutedCommands != 0);

    [self.webView stringByEvaluatingJavaScriptFromString:@"cordova.commandQueueFlushing = false"];
}

ということで

  • iframe fire
  • big size data support
  • queuing

の3つを実装することでより優れたJS-Native Bridgeがつくれるでしょう!

その他

callbackのやり方

JSONで渡すデータ内にcallbackようのJSコード文字列を渡してevalしてもらうとか.他色々

画像などのバイナリデータの渡し方

Base64エンコードしてJSONにつっこむ

UIWebViewのデフォルトUserAgent

WebページのUAを解析しようとしたとき、

iOSでMobile SafariとUIWebView(それ以外のアプリ内ブラウザ)の違いって検出できないのかなー」

と思ったことないですか


UIWebViewのUAは変更可能なのであまりアテにならないのですが、
何も指定しないときは、Mobile Safariとよく似ていて、少しだけ異なるUAになります


Mobile Safari:
Mozilla/5.0 (iPhone; U; CPU iPhone OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Mobile/9A334 Safari/7534.48.3

UIWebView:
Mozilla/5.0 (iPhone; U; CPU iPhone OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Mobile/9A334
Mobile Safari:
Mozilla/5.0 (iPad; U; CPU OS 4_3_5 like Mac OS X; ja-jp) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8L1 Safari/6533.18.5

UIWebView:
Mozilla/5.0 (iPad; U; CPU OS 4_3_5 like Mac OS X; ja-jp) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8L1

末尾のSafari/xxxx.xx.xがなくなるみたいです。
WebKitを使ったブラウザコンポーネントでSafariじゃねーよってことみたいですね