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

shimada-kの日記

ソフトウェア・エンジニアのブログです

IBM Blockchainでソリッドステートを作る

プログラミング

この記事は「ネクスト(Lifull) Advent Calendar 2016」21日目の記事として書かれました。

qiita.com

前回の投稿からかなり時間が空いてしまいました。今年も年の瀬ですね。

なぜ年の「瀬」というかというと、「瀬」には「流れの速い」という意味があるそうです。またツケ払いが多かった江戸時代には年末にツケの清算がなされたことから、めまぐるしい時期ということで年末のことを「年の瀬」ということになったらしいです。

はじめに

私はFinTech系の事業に携わっておりますので、ぜひブロックチェーンについて議論したいと思いました。

昨年にはマウントゴックスの事件などありキナ臭い印象が広がっているかもしれません。しかしマウントゴックスの破綻とブロックチェーンの技術自体には何の因果関係もありません。

2016年には三菱UFJが仮想通貨に参入したり、2017年度の税制改正ではビットコインの購入に際しては消費税を課税しないなど、ブロックチェーンを使用した仮想通貨は徐々に社会的な認知度をあげていると言えます。

闇雲にブロックチェーンについてレポートを書いてみるものいまいちモチベーションが上がりませんので、今回は「攻殻機動隊 S.A.C. SSS」のソリッドステートを取り上げてみることにしてみました。

フライト記録は改竄しておけよ

最低限の説明は必要でしょう。「攻殻機動隊 S.A.C. SSS」は2011年に公開された劇場版・攻殻機動隊です。「攻殻機動隊 S.A.C. 2nd GIG」で草薙素子が9課を去ってから2年後が舞台となっており、少子高齢化社会を背景にして新たな組織となった9課の苦悩と、超ウィザード級ハッカー傀儡廻(くぐつまわし)」との電子戦が描かれています。

sss.ph9.jp

ソリッドステートとは全自動老人介護システム内の誘拐インフラのことです。虐待を受けている子供を身寄りのない老人の子供としてデータを書き換えるシステムを指しています。

f:id:shimada-k:20161220232356p:plain

少子高齢化時代の対策として、厚生労働省の職員が開発したシステムとされています。

少子化が叫ばれるなか、年間5万人もの6歳未満の子供が無意味に命を落としていることを。

児童相談所や警察など関係機関が事態を把握していたにもかかわらず防ぐことができなかったものだ。

どこかで聞いたような話です。2011年の作品ながら、現在を言い当てているようです。

ソリッドステートの本質は記憶を改竄することだと思います。なにせ子供の親を入れ替えないといけないのですから。親と子供の記憶は最低限書き換えが必要ですし、親類やその他周辺関係者の記憶も書き換える必要があるでしょう。

警告を無視したな

さて、本題に入りましょう。ブロックチェーンを使った仕組みを作るということは、チェーンコードを書くということです。今回はIBM Blockchainを使います。

IBM BlockchainとはIBM Bluemix上で動かせるアプリケーションフレームワークです。GUIで操作できAPIを叩いてデプロイできます。IBM BluemixとはIBMが提供するPaaSです。30日間の無料トライアル期間があります。

IBM Bluemix http://www.ibm.com/developerworks/jp/bluemix/

IBM Blockchainのサービスでは4つのピアを動かすことができます。

f:id:shimada-k:20161220234608p:plain

IBM Blockchainで自作のチェーンコードを動かす場合、APIメニューを使用することになります。

f:id:shimada-k:20161220234618p:plain

APIメニューではリクエストを送信して検証するためのUIを提供しています。curlを使って自前でリクエストすることもできますが、開発時はこちらを使ったほうが格段にはかどります。

委ねてみるわ ゴーストの囁きに

チェーンコード

IBM Blockchainでは Hyperledger Fabricというブロクチェーンを基盤に採用しています。本家のHyperledgerプロジェクトから派生したインキュベーションプロジェクト、という位置づけです。オープンソースで公開されています。

Hyperledger Project https://www.hyperledger.org/

HyperledgerではDeploy,Invoke,Queryの3種類のメソッドを使います。Deployはデプロイ時に使用するもの、Invokeはデータの新規追加、更新、削除。Queryは参照です。

今回はInvokeでデータを書き換え、他のピアでもデータが書き換わっていることをQueryで確認します。

IBM Blockchain上で動いている4つのピアは個々人の記憶の塊だと見立てます。

記憶のデータは以下のような構造にしました。戸籍データのようなものです。

type Family struct{
    Sex int `json:"sex"`
    Birthday string `json:"birthday"`
    SpouseId string `json:"spouse_id"`
    FatherId string `json:"father_id"`
    MotherId string `json:"mother_id"`
    ChildId string `json:"child_id"`
}

データの改竄フローは以下のように実装しました。

  1. 父の持つ子供IDを消す(hospital)
  2. 母の持つ子供IDを消す(hospital)
  3. 子供の両親IDを消す(plugged)
  4. 子供の父親IDを老人IDに変更する(adopted)

ちなみに「攻殻機動隊 S.A.C. SSS」では老人は理解した上でソリッドステートに加担していたので、作品中では明確にされていませんが老人は自分の子だとは認識していなかったと思われます。そのため老人のデータ(記憶)は変更しないことにします。

実装したチェーンコードはgistにあげておきました。

動かしてみる

下記のリスクエストでデプロイします。IBM BlockchainのAPIメニューから行います。ピアは0を指定しています。ユーザはuser_type1_0でログインしています。

 {
     "jsonrpc": "2.0",
     "method": "deploy",
     "params": {
         "type": 1,
         "chaincodeID": {
             "path": "https://github.com/shimada-k/learn-chaincode/finished"
         },
         "ctorMsg": {
             "function": "init",
             "args": [
                 "hello"
             ]
         },
         "secureContext": "user_type1_0"
     },
     "id": 0
 }

データを準備する

Invoke:addを使用してデータを追加します。あらかじめ老人、子供、子供の父母のデータを作成しておきました。

老人

老人は80歳という設定です。身寄りのない老人ということで配偶者と子供はありません。

{
  "jsonrpc": "2.0",
  "result": {
    "status": "OK",
    "message": "{\"sex\": 1,\"birthday\": \"1936-12-21\",\"spouse_id\": \"\",\"father_id\": \"8b77fb747eda782e202f85f3c7e5df850200977a053527fccbc3914d50ee98b5\",\"mother_id\": \"194f138b39f10252c32bd1cc95b9144da7688954c56eb014c3b7c4759ce33735\",\"child_id\": \"\"}"
  },
  "id": 0
}
子供の父

父親は38歳(1978年生まれ)です。

{
  "jsonrpc": "2.0",
  "result": {
    "status": "OK",
    "message": "{\"sex\": 1,\"birthday\": \"1978-02-20\",\"spouse_id\": \"57001488396447b13c743ec4b3e6f7c6f01a53022c7141a8188d6677bf599fa4\",\"father_id\": \"786580bfbb1477f9f00c2bb3566ad52f204067d52d2756a9b4ea408ffbe74152\",\"mother_id\": \"e81727115da92778c19055bd2839bf6c3105224062958e28d7d2317de8a7cdd6\",\"child_id\": \"65152f7fb723e1f385bab62bdc18673209e2324f356f895bf516365cc62834e2\"}"
  },
  "id": 0
}
子供の母

母親は36歳です。sex:1は男性、sex:2は女性としています。

{
  "jsonrpc": "2.0",
  "result": {
    "status": "OK",
    "message": "{\"sex\": 2,\"birthday\": \"1980-08-14\",\"spouse_id\": \"7b451b73ce0614147b01edb102f318a7496ebb533dde28e1fd80c370770f6279\",\"father_id\": \"ff5b8a198f9f96f89ca2aa483c927d58ab7f42563df7c24b47992fdf3cddccea\",\"mother_id\": \"c2dff7c37bbc245dc93ce572476254bd287860e1cebd2b5f36181748fd1f5a54\",\"child_id\": \"65152f7fb723e1f385bab62bdc18673209e2324f356f895bf516365cc62834e2\"}"
  },
  "id": 0
}
子供

子供は6歳です。当然ながら配偶者と子供はいません。father_idとmother_idはすでに追加したIDを登録しています。

{
  "jsonrpc": "2.0",
  "result": {
    "status": "OK",
    "message": "{\"sex\": 1,\"birthday\": \"2010-11-03\",\"spouse_id\": \"\",\"father_id\": \"7b451b73ce0614147b01edb102f318a7496ebb533dde28e1fd80c370770f6279\",\"mother_id\": \"57001488396447b13c743ec4b3e6f7c6f01a53022c7141a8188d6677bf599fa4\",\"child_id\": \"\"}"
  },
  "id": 0
}

データが揃ったのでいよいよ理不尽に損なわれてしまうゴーストの再利用を一部始終、追体験してみましょう。 Invokeでデータを書き換え、Queryで書き換わったことを確認します。

まずはデータの書き換えからです。リクエストするJSONを示します。

父の持つ子供IDを消す(hospital)

{
  "jsonrpc": "2.0",
  "method": "invoke",
  "params": {
    "type": 1,
    "chaincodeID": {
      "name": "a027a646d118ca8513ae6854ca455ab6d3c1bf0c6ce331dfc384a7a406605114e2be6c7c46abbb3aaec8986a93327b8dbb5b2c399b3a7ff02eea48dac7184e74"
    },
    "ctorMsg": {
      "function": "hospital",
      "args": [
        "7b451b73ce0614147b01edb102f318a7496ebb533dde28e1fd80c370770f6279"
      ]
    },
    "secureContext": "user_type1_0"
  },
  "id": 0
}

母の持つ子供IDを消す(hospital)

{
  "jsonrpc": "2.0",
  "method": "invoke",
  "params": {
    "type": 1,
    "chaincodeID": {
      "name": "a027a646d118ca8513ae6854ca455ab6d3c1bf0c6ce331dfc384a7a406605114e2be6c7c46abbb3aaec8986a93327b8dbb5b2c399b3a7ff02eea48dac7184e74"
    },
    "ctorMsg": {
      "function": "hospital",
      "args": [
        "57001488396447b13c743ec4b3e6f7c6f01a53022c7141a8188d6677bf599fa4"
      ]
    },
    "secureContext": "user_type1_0"
  },
  "id": 0
}

子供の両親IDを消す(plugged)

{
  "jsonrpc": "2.0",
  "method": "invoke",
  "params": {
    "type": 1,
    "chaincodeID": {
      "name": "a027a646d118ca8513ae6854ca455ab6d3c1bf0c6ce331dfc384a7a406605114e2be6c7c46abbb3aaec8986a93327b8dbb5b2c399b3a7ff02eea48dac7184e74"
    },
    "ctorMsg": {
      "function": "plugged",
      "args": [
        "65152f7fb723e1f385bab62bdc18673209e2324f356f895bf516365cc62834e2"
      ]
    },
    "secureContext": "user_type1_0"
  },
  "id": 0
}

子供の父親IDを老人IDに変更する(adopted)

{
  "jsonrpc": "2.0",
  "method": "invoke",
  "params": {
    "type": 1,
    "chaincodeID": {
      "name": "a027a646d118ca8513ae6854ca455ab6d3c1bf0c6ce331dfc384a7a406605114e2be6c7c46abbb3aaec8986a93327b8dbb5b2c399b3a7ff02eea48dac7184e74"
    },
    "ctorMsg": {
      "function": "adopted",
      "args": [
        "65152f7fb723e1f385bab62bdc18673209e2324f356f895bf516365cc62834e2",
        "2d9dc1dcf4330422f5594f0325b01b9326df7554c58f34a29e51a43970d5bbbd"
      ]
    },
    "secureContext": "user_type1_0"
  },
  "id": 0
}

次にデータが書き換わったことをQueryで確認しましょう。

両親のデータを確認する

父親
{
  "jsonrpc": "2.0",
  "result": {
    "status": "OK",
    "message": "{\"sex\":1,\"birthday\":\"1978-02-20\",\"spouse_id\":\"57001488396447b13c743ec4b3e6f7c6f01a53022c7141a8188d6677bf599fa4\",\"father_id\":\"786580bfbb1477f9f00c2bb3566ad52f204067d52d2756a9b4ea408ffbe74152\",\"mother_id\":\"e81727115da92778c19055bd2839bf6c3105224062958e28d7d2317de8a7cdd6\",\"child_id\":\"\"}"
  },
  "id": 0
}

子供IDが消えていますね。

母親のデータ
{
  "jsonrpc": "2.0",
  "result": {
    "status": "OK",
    "message": "{\"sex\":2,\"birthday\":\"1980-08-14\",\"spouse_id\":\"7b451b73ce0614147b01edb102f318a7496ebb533dde28e1fd80c370770f6279\",\"father_id\":\"ff5b8a198f9f96f89ca2aa483c927d58ab7f42563df7c24b47992fdf3cddccea\",\"mother_id\":\"c2dff7c37bbc245dc93ce572476254bd287860e1cebd2b5f36181748fd1f5a54\",\"child_id\":\"\"}"
  },
  "id": 0
}

こちらも子供IDが無くなっています。

子供のデータを確認する

子供のデータを確認してみましょう。父親が老人のIDに書き換わっているはずです。

{
  "jsonrpc": "2.0",
  "result": {
    "status": "OK",
    "message": "{\"sex\":1,\"birthday\":\"2010-11-03\",\"spouse_id\":\"\",\"father_id\":\"2d9dc1dcf4330422f5594f0325b01b9326df7554c58f34a29e51a43970d5bbbd\",\"mother_id\":\"\",\"child_id\":\"\"}"
  },
  "id": 0
}

いいですね。父親IDが老人のIDに書き換わり、母親IDは空です。

別のピアで確認してみる

ブロックチェーンですから、他のピア上でも同一のデータ処理が行われているはずです。

上述の処理はピア0で行っているのでピア1でも確認してみます。APIメニューの上部でピアを切り替え、registerでuser_type2_0でログインしてQueryしてみましょう。

父親のデータ

request

{
  "jsonrpc": "2.0",
  "method": "query",
  "params": {
    "type": 1,
    "chaincodeID": {
      "name": "a027a646d118ca8513ae6854ca455ab6d3c1bf0c6ce331dfc384a7a406605114e2be6c7c46abbb3aaec8986a93327b8dbb5b2c399b3a7ff02eea48dac7184e74"
    },
    "ctorMsg": {
      "function": "read",
      "args": [
        "7b451b73ce0614147b01edb102f318a7496ebb533dde28e1fd80c370770f6279"
      ]
    },
    "secureContext": "user_type2_0"
  },
  "id": 0
}

response

{
  "jsonrpc": "2.0",
  "result": {
    "status": "OK",
    "message": "{\"sex\":1,\"birthday\":\"1978-02-20\",\"spouse_id\":\"57001488396447b13c743ec4b3e6f7c6f01a53022c7141a8188d6677bf599fa4\",\"father_id\":\"786580bfbb1477f9f00c2bb3566ad52f204067d52d2756a9b4ea408ffbe74152\",\"mother_id\":\"e81727115da92778c19055bd2839bf6c3105224062958e28d7d2317de8a7cdd6\",\"child_id\":\"\"}"
  },
  "id": 0
}

・子供のデータ

request

{
  "jsonrpc": "2.0",
  "method": "query",
  "params": {
    "type": 1,
    "chaincodeID": {
      "name": "a027a646d118ca8513ae6854ca455ab6d3c1bf0c6ce331dfc384a7a406605114e2be6c7c46abbb3aaec8986a93327b8dbb5b2c399b3a7ff02eea48dac7184e74"
    },
    "ctorMsg": {
      "function": "read",
      "args": [
        "65152f7fb723e1f385bab62bdc18673209e2324f356f895bf516365cc62834e2"
      ]
    },
    "secureContext": "user_type2_0"
  },
  "id": 0
}

response

{
  "jsonrpc": "2.0",
  "result": {
    "status": "OK",
    "message": "{\"sex\":1,\"birthday\":\"2010-11-03\",\"spouse_id\":\"\",\"father_id\":\"2d9dc1dcf4330422f5594f0325b01b9326df7554c58f34a29e51a43970d5bbbd\",\"mother_id\":\"\",\"child_id\":\"\"}"
  },
  "id": 0
}

ピア1でも同一のデータが得られました。これで他のピアでも同期が取れていることが分かりました。これで記憶の改竄がおこなれ、無事に誘拐が完了されました。

攻殻機動隊 S.A.C. SSS」においても、カ・ルマのアジトにいた子供達を見て、ボーマが「記憶の一部が消されていて、IDが別の物に書き換えられているらしい事も分かった。」と言っています。

おわりに

今回はIBM Blockchainを使用してブロックチェーンを利用した仕組みを実装してみました。ハマったところをいくつか書いておきます。

開発はOSX環境で行いましたが、home brewでインストールしたGoのバージョンが古くてビルドできなくて悩みました。IBMのサンプルプロジェクトではGo 1.6推奨と書かれていますがOSX Sierraでは1.6でもコンパイルできませんでした。最新版を入れた方が手戻りが少ないかもしれません。

IBMのサンプルプロジェクトではビルドの環境も作っていますが、チェーンコードをデプロイするだけであれば不要です。ただデプロイして動かなかった場合に切り分けするために、ビルド環境を作りデプロイ前にビルドすることを強くお勧めします。

またデプロイ時のレスポンスでchaincodeIDが帰ってくるのですが、これが毎回同じだとデプロイでチェーンコードが変化していないことになります。

原因は/finishedの中身を更新していなかったことです。/startの中にスケルトンコードが入っているのでその中をひたすらcommit & pushしていたのですが、それでは当然ながら/finishedの中が変化していません。英語のチュートリアルを流し読みしていると気づきませんでした。

IBM Bluemixが本来有料のサービスということもありますが、今後はもっとローカライズされた情報が充実していき、より多くの人にとって敷居の低いものになっていくといいですね。

冒頭でも書きましたが、「攻殻機動隊 S.A.C. SSS」は社会問題を先取りして捉えています。2016年には「保育園落ちた日本死ね!!!*1」というブログ記事が公開され国会で話題になりました。待機児童の問題は主に都市部が深刻ですが、負担にあえぐ現役世代の声が顕在化した事象といえます。

一方高齢化問題としては、2025年問題を控えて社会保障制度の見直しが喫緊の課題です。2017年度から医療保険介護保険の見直しが入ります。年金カット法案と称されつつも国民年金法改正案*2が採決されました。

2017年はどのような年になるでしょうか。とりあえず都内に保育園が増えてくれるとありがたいですね。少子化問題社会保障制度の見直しを含めた高齢化問題は引き続き最優先事項です。

それにしても、ネットは広大だわ。もう既に、私達の知らない次の社会が生まれ始めている・・・


本記事は犯罪としての誘拐を肯定又は助長するものではありません

GCDの実行ポリシーとキューの種類まとめ

プログラミング

今更ながらGCDを勉強しました。Grand Central Dispatchです。わからなければ自分で使ってみることです。そうしたらいざという時に役に立ちます。

調査

GCDには主に2種類のキューのタイプと2種類のディスパッチポリシーがあります。組み合わせで合計4種類の使い方が存在します。

  1. 非同期実行&並列キュー
  2. 非同期実行&直列キュー
  3. 同期実行&並列キュー
  4. 同期実行&直列キュー

webの資料を読んでもいまいちイメージがつかなかったので以下のようなコードで4パターン検証してみました

環境

#include <stdio.h>
#include <pthread.h>
#include <time.h>
#include <sys/time.h>
#include <stdlib.h>
#include <dispatch/dispatch.h>

#define CEIL_MAX    100000
#define NUM_THREAD  20
#define CONTEXT_PRINT   1 

// 1からceilまでの間で素数の個数を計算する関数
int prime(int ceil, int thread_order)
{
    int i, j, num_yakusu, prime = 0;

    for(i = 1; i <= ceil; i++){
        num_yakusu=0;

        for(j = 1; j <= i; j++){
            if(i % j == 0){
                num_yakusu++;
            }
        }
        
        // 1とそれ自身でしか割り切れなかったら素数
        if(num_yakusu == 2){
            prime++;
            if(CONTEXT_PRINT){
                printf("thread order:%d\n", thread_order);
            }
        }
    }
    return prime;
}

// entry point
int main(int argc, char *argv[]){
    int i;

    //dispatch_queue_t q = dispatch_queue_create("gcd.sample.osx", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t q = dispatch_queue_create("gcd.sample.osx", DISPATCH_QUEUE_SERIAL);
    //dispatch_group_t group = dispatch_group_create();

    srand((unsigned)time(NULL));
    printf("loop start\n");

    for(i = 0; i < NUM_THREAD; i++){
        dispatch_group_async(group, q,
        //dispatch_sync(q,
            ^{
                pthread_t p = pthread_self();
                struct timeval tp;
                int ceil = rand() % CEIL_MAX + 1;
                int num_prime = prime(ceil, i);
                int j = i;
                struct timeval tq;

                printf("i:%d, thread id:%p ceil:%d num_prime:%d\n", i, p, ceil, num_prime);    
            }
        );
        printf("thread done\n");
    }
    //dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    printf("loop end\n");
}

1からceilまでの数値で素数がいくつあるか調べる処理をGCDを使って並列処理するコードです。

GCDの実行ポリシーとキューの種類の4パターンでソースコードを切り替え、生成されるスレッドの個数とディスパッチの対象を調べました。

パターン スレッドの個数 ディスパッチの対象
1 5 インキューされているすべてのタスク
2 2 実行中のタスクの次にインキューされたタスク
3 1 実行中のタスクの次にインキューされたタスク
4 1 実行中のタスクの次にインキューされたタスク

考察

パターン1と2は非同期実行です。1の場合は非同期実行&並列キューです。タスク同士が完全に独立で並列に実行されます。スレッドはメインスレッドの他に4スレッド生成されて、タスクすべてがコンテキストスイッチされて実行されます。

2の場合は非同期実行&直列キューです。メインスレッドの他に1つスレッドが生成されてそのスレッド内でインキューされた順番に実行されていきます。パターン1と2のケースは両方とも親スレッドとは別個にスレッドが作成されています。これは非同期実行ポリシーの特徴と言えます。

パターン3と4は同期実行です。3の場合は同期実行&並列キューです。並列キューなので複数スレッドが立ち上がると見せかけて実際起動するのは1スレッドだけです。

dispatch_syncはそれぞれのブロックの実行が完了するまで終わらないのです。なので、並列キュー的には複数スレッドを立てたいけど、処理できるブロックが1個ずつだから結果として1個しかスレッドが 立たないということになります。

4の場合は同期実行&直列キューです。これは言わずもがな純度100%の直列&同期実行です。インキューされた順番に1つ1つタスクが完了するまで次のタスクは実行されません。パターン3と4のケースは子スレッドが作成されるわけではありませんので親スレッドはブロックされます。

まとめ

実行結果より、キューの種類よりも実行ポリシーのほうが支配的であると言えます。

また「同期」と「直列」という単語が2種類出てくるのでわかりにくくなっているのですが、「同期」というのはタスク間に対して使われているものではなく、メインスレッドとの関係性を表す単語です。

そして「直列」というのはタスクの順番のみを表す単語として使用されています。少なくともGCDにおいては。

親スレッドとは別なところでCPUを全て使って複数処理を同時に走らせる(いちばんイメージしやすいパターン)なら並列&非同期の組み合わせ一択。親スレッドは自由にしたいというだけの場合は直列&非同期でも可。

むしろ上記以外のケースだった場合、GCD使ってもメリット薄い気がしますね。。

sched_setaffinityを使って非同期&並列キュー環境で全て同じCPUに固定した場合どうなるのか試したかったんですが、OSXでは使えませんでした。残念。

久しぶりにC言語を使いました。楽しかったです。

UIScrollViewにおけるcontentOffsetの注意点

プログラミング

UINavigationControllerを使用している場合で、UIScrollViewをpagingで使用している時にコードからUILabelを置こうとした時に想定通りの座標で配置できなかったことがあったので解決手段をメモしておきます。

既知のトピックではありますが、ちゃんとまとまった情報に出会えなくてかなりハマったので書いておきます。

これは2014年iOSアドベントカレンダー5日目の記事として書かれました。

環境

コード

#import "ViewController.h"

@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIScrollView *scrollView;

@end

#define NUM_LABEL 5

@implementation ViewController

- (void)viewDidLayoutSubviews {
    NSLog(@"viewDidLayoutSubviews");
    [super viewDidLayoutSubviews];
    
    //スクロールの総範囲の設定
    NSInteger kScrollObjWidth = [[UIScreen mainScreen] applicationFrame].size.width;
    [self.scrollView setContentSize:CGSizeMake(NUM_LABEL * kScrollObjWidth, self.scrollView.frame.size.height)];
    // 謎に下にずれることがあるのでy座標を0にする
    NSLog(@"contetOffset (%f, %f)", self.scrollView.contentOffset.x, self.scrollView.contentOffset.y);
    self.scrollView.contentOffset = CGPointMake(self.scrollView.contentOffset.x, 0);
}

- (void)viewDidAppear:(BOOL)animated
{
    NSLog(@"viewDidAppear");
    [super viewDidAppear:animated];
    //[self showObjects];
}

- (void)viewDidLoad {
    NSLog(@"viewDidLoad");
    [super viewDidLoad];
    self.scrollView.contentOffset = CGPointMake(self.scrollView.contentOffset.x, 0);
    // Do any additional setup after loading the view, typically from a nib.
    [self showObjects];
}

- (void)showObjects{
    NSInteger kScrollObjWidth = [[UIScreen mainScreen] applicationFrame].size.width;
    NSInteger i;
    
    for(i = 0; i < NUM_LABEL; i++){
        UILabel *label = [[UILabel alloc] init];
        
        label.frame = CGRectMake(kScrollObjWidth * i, 0, kScrollObjWidth, 30);
        
        label.textAlignment = NSTextAlignmentCenter;
        label.text = [NSString stringWithFormat:@"%ld", (long)i];
        
        label.backgroundColor = [UIColor greenColor];
        
        [self.scrollView addSubview:label];
    }
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

このコード。上記のようにshowObjectsをviewDidLoadで呼び出すと下記のようになります。その場合、呼び出す前にcontentOffsetの初期化をやっても同様です。

オレンジ色のものがUIScrollViewで、緑色のものがUILabelです。

f:id:shimada-k:20141203013948p:plain

また、viewDidLayoutSubviewsでcontentOffsetを初期化しない場合でも同じ結果になります。コードからではなくInterface Builderでオブジェクトを置いている場合でも同様です。

ちなみに上記の状態でもマウスか指でスクロールして何かしらの動きを与えると座標が0の状態(想定通りの状態)に戻ります。

発生理由と対策

発生理由はNavigationControllerVilew配下にScrollViewを置いた場合、ContentsOffset.yのデフォルト値が-64になっているからです。

f:id:shimada-k:20141203012446p:plain

-64というのはステータスバーの20ptとナビゲーションバーの44ptの合計値で、NavigationController配下にあった時UIScrollViewのコンテンツが隠れてしまわないようにという配慮です。これはUIScrollViewの仕様です*1

ちなみにナビゲーションバーが存在しない状態だとcontentsOffsetの初期値は(0.000000, -20.000000)とはならずに(0.000000, 0.000000)となります。

解決するには

  • UIScollViewが配置され(てい)るUIViewControllerのautomaticallyAdjustsScrollViewInsetsをNOにする

もしくは

  • UIScrollViewのcontentsOffsetをviewDidLayoutSubviewsで初期化した後にshowObjectsを呼ぶ

必要があります。例えばcontentsOffsetをviewDidLayoutSubviewsを初期化してshowObjectsをviewDidAppearで呼び出すようにするとscrollViewの左上からちゃんとUILabelが表示されます。

f:id:shimada-k:20141203014005p:plain

この件はUIScrollViewに対してConstrainsを設定していても発生します。またタップすると(0, 0)に戻るという点と、ナビゲーションバーが存在しない状況では発生しないという点が邪魔をして、解決に時間がかかりました。

東京メトロのアプリコンテストに応募しました

プロモーション プログラミング

iOSアプリを作成しアプリコンテストに応募しました。作成から応募までのログを宣伝を兼ねて書いておきます。

東京メトロ オープンデータ活用コンテスト

東京メトロAPIを使用したアプリを作るという趣旨です。サマって書くと個人・団体問わず参加OK、一人何作品出してもOK。Web/iOS/Android/WindowsPhoneで作ってね、賞金出すよ。というもの。僕らはiOSアプリを作りました。選考要素にデザインも含まれていたので社内のクリエイティブの友達を誘いプライベートの時間を使って2人体制で開発しました。

作ったもの


MetroCluster紹介動画 - YouTube

foursqureのAPIを使って電車の運行状況に応じて到着駅付近のvenueの画像と地図を表示するアプリを作りました。socket.ioを使ったチャット機能もつけてアプリのユーザ同士で会話できる機能もつけました。

感想

かなり詰みました。着手したのが9月末からで、スケジュールを切ったところAppleの審査(このことは後で詳細を書く)があるので10月中にベータ版を作ることに。開発期間は実質1ヶ月という状況。10月は仕事以外でも予定が多い月だったので開発時間の捻出に苦労しました。

Apple審査

マイルストーンに従ってβ版を10月下旬に審査に出しました。まあベータ版なので速攻でリジェクトを食らいました。ベータ版を修正&機能追加した版を再度11/2に審査に提出しました。

コンテストの応募締め切りが11月17日だったので最悪Apple審査が締め切りに間に合わない可能性があると思いコンテストの運営MLに審査が間に合わない場合の運営のケアはあるかどうか問い合わせました。

結果はNG。iOSアプリの場合、応募時点でAppStoreに公開さていることが条件と言われました。まあダメ元で聞いてみたのであまり期待してませんでした。

Apple次第ということで待っていると11月11日にApple審査が通りました。Ready for Sale。これで応募に関しては気に病むことはないと安堵したのもつかの間。AppStoreから落としてきたアプリを起動するとクラッシュ。メイン画面まで辿り着かないという状況に。

原因箇所は位置情報の取得を待つところで、無限ループに突入していました。申請前に一度アプリをアンインストールしてテストしなかったのが原因でこのバグを捕捉できていませんでした。

Appleの審査は新規よりもアップデートの方が申請通過が早いらしいという情報は事前に得ていたのでもうその可能性に賭けるしかないと思い、速攻で修正し再度申請。この時点で申請は3回目。日付は11/12日。

ただそう甘く無いらしく応募2営業日前になってもiTunes Connectは音沙汰なし。これはもうしょうがないと思い、緊急レビューの禁じ手を使う覚悟を決めました。緊急レビューは依頼しても確実にやってくれるものではないので、一旦Appleの返答待ちになりました。

緊急レビュー依頼の結果は、NG。まあ個人都合だから断られてもしかたないと思いつつも、絶望。

もう手を尽くしたので、あとはコンテストの運営側を説得するしかないと思い開発者サイトを見ると、iOSアプリケーションの場合は締め切りまでにAppleにSubmitしたバイナリに関しては審査対象とする旨のアナウンスが!

僕が問い合わせたように他の応募者からも同様の問い合わせが来ていたようで、ダメ元で問い合わせたのが良かったのか、iOSアプリを作成している応募者は応募時と審査通過時にiTunes Connectのステータス履歴をPDFにしたものを送ればコンテストの運営側が確認するのでOKとのこと。

もう神に救われた気分でしたよ。ええ。

それからは直したい部分の改修と時間がなくて追加できなかった機能を追加してコードフリーズ。前回の失敗を活かし申請前に地下鉄に乗ってデバッグ。締切日が月曜日だったので前日までに申請と応募の素材と文章をそろえました。

そして本日無事Apple申請に通過しました。実際にDLしてみたところ前回のようにクラッシュするケースはありません。かなりバタつきましたが作成したアプリをApple申請に出したのは初めての経験だったので勉強になりました。

神から祝福を受けたアプリなので是非使ってみてください。

MetroCluster

MetroCluster

  • KATSUYA SHIMADA
  • ライフスタイル
  • 無料

2015-07-20 追記

上記アプリはAPIの公開期限が過ぎたためAppストアから削除しました

Synthでejsを使う

フロントエンド メモ

Synthでテンプレートエンジンにejsを使用する時のメモ。Synthのバージョンは0.5.2です。

back/node_modules/synth/synth.jsに追記

back/package.jsonにejsの項目を追記してsynth install -bを実行します。その後、back/node_modules/synth/synth.jsにejsを使うように変更します。

/* the main synth init function */
exports = module.exports = function (options) {
  options = options || {};
  var defaultResourceDir = path.join(process.cwd(), 'back/resources');
  var viewDir = options.viewDir || path.join(process.cwd(), 'front');
  //var viewEngine = options.viewEngine || 'jade';
  var viewEngine = options.viewEngine || 'ejs';

front/index.ejsを作成

index.jadeがレンダリングした結果をもとに勝手にejs化しました。

<!DOCTYPE html>
<html ng-app="my_app">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title><%= appName %></title>
        <% for(var i = 0; i < cssFiles.length; i++){ %>
            <link rel="stylesheet" href=<%= cssFiles[i] %>>
        <% } %>

        <!-- Preloaded Data-->
        <script>
            var preloadedData = <%- data %>;
        </script>

        <% for(var i = 0; i < jsFiles.length; i++){ %>
            <script type="text/javascript" src="<%= jsFiles[i] %>"></script>
        <% } %>
    </head>
    <body>
        <h1><%= appName %></h1>
        <div ng-view>
            <% if(preloadHTML){ %>
                <script type='text/ng-template', id=<%= preloadHTMLPath %>>
            <% } %>
        </div>
    </body>
</html>

node_modules/synth/lib/frontendRenderer.jsの改修

front/index.ejsとfront/html/hoge/getIndex.ejsを読みに行くようにコードを書きます。まずはejsをrequireしましょう。

var path = require('path'),
    Promise = require('bluebird'),
    fs = Promise.promisifyAll( require('fs') ),
    jade = require('jade'),
    ejs = require('ejs'),
    assets = require('./assets.js');

64行目付近に拡張子を置換してjadeファイルを読みに行っている場所があるので、ejsに置換するコードを追加し、さらにejs.renderFileを使ってejsファイルを読んであげるようにします。

var readHTML = function (path) {
  var production = process.env.NODE_ENV == 'production';
  return fs.readFileAsync(path).error(function (err) {
    if (err.cause.code == 'ENOENT') {
      // Couldn't find the HTML version, how about jade?
      ejs.renderFile(path.replace(/\.html$/, '.ejs'), {}, function(err, result) {
        if(!err){
            console.log(result);
        }
        else{
            console.log(err);
        }
      });
      //return jade.renderFile( path.replace(/\.html$/, '.jade'), {
      //  pretty: !production
      //});
    } else throw err;
  })
  .catch(function (err) {
    if (err.code != 'ENOENT') throw err;
    return null;
  });
};

front/html/tweets/getIndex.ejs追加

frontendRenderer.jsを書き換えてもファイルを作らないと読んでもらえないのでファイルを作りましょう。これもindex.ejsと同様にjadeでレンダリングされたあとのHTMLを参考にejs化しました。

<ul class="tweet-timeline">
  <li ng-repeat="tweet in tweets" class="tweet">
    <div class="content">{{ tweet.content}}</div>
    <div class="date">{{ tweet.created_at | date:'medium' }}</div>
  </li>
</ul>

以上です。あとはsynth sしてlocalhost:3000/tweetsにアクセスすればデフォルトのjade版と変わらない形でレンダリングされるはず。

UnityでS.S.デッドリー・ボンバーを作ってみる

フロントエンド

人造人間13号*1が放つS.S.デッドリーボンバーをUnityで再現してみました。人造人間13号はドクター・ゲロが使っていたマシンが悟空抹殺のために自動で作りだした人造人間です。

人造人間13号

構造は、まず真ん中のコアのような球体と、その外側に赤い帯が表示されている球体、さらに一番外側には透明な球体の3つのsphereオブジェクトを作成しました。赤い帯のような模様はテクスチャで表現しました。

透過テクスチャなのでサーフェスシェーダーでマルチパスレンダリングを行います。シェーダーのコードはこちらを参考にしました。

1番外側のsphereには6個のparticle systemを子オブジェクトに設定してあります。原作のようにサイズ差のあるパーティクルにするためにstartSizeをCurveで変更してあります。

6個のparticle systemのうち4個をスクリプト上で180フレームごとにランダムに出し分けています。

using UnityEngine;
using System.Collections;

public class SSDBOuterController : MonoBehaviour {

    public GameObject[] particle_lines;

    private System.Random r;
    private int[] particle_status = new int[6];
    private int particle_interval = 0;

    // Use this for initialization
    void Start () {

        r = new System.Random(1000);

        for(int i = 0; i < particle_lines.Length; i++){
            particle_lines[i].particleSystem.Pause();
        }

    }
    
    // Update is called once per frame
    void Update () {

        if(particle_interval < 180){
            gameObject.transform.Rotate (new Vector3((float)r.Next (2, 8), (float)r.Next (2, 8), (float)r.Next(2,8)));
            particle_interval++;
        }
        else{
            // 一旦すべて表示状態にする
            for(int i = 0; i < particle_status.Length; i++){
                particle_lines[i].particleSystem.renderer.enabled = true;
                particle_status[i] = 0;
            }

            int counter = 0;

            // ランダムで1/3を選んで消す
            while(counter < (particle_lines.Length / 3)){
                int index = r.Next(0, particle_lines.Length - 1);
                if(particle_status[index] == 0){
                    particle_status[index] = 1;
                    particle_lines[index].renderer.enabled = false;
                    counter++;
                    //Debug.Log (index);
                }
            }
            particle_interval = 0;
        }
    }
}

赤い帯の回転と周りの光の筋の回転が同期していないのが原作において仕様であることと、particle systemを固定座標で回転させると見た目がワンパターンになってしまうのでこのような実装にしました。

さらに原作では光の筋の長さが一定ではないため、particle systemの設定を6本のうち2本だけ変えて2倍の長さにしてあります。

f:id:shimada-k:20140831011614p:plain

DEMO

赤い帯のsphereと真ん中のコアの周りにただよう拡散光はHaloで表現しました。

ついでにskyboxを設定しました。skyboxのテクスチャはImport Packageで取り込まれるデフォルトのテクスチャです。

私はドラゴンボールに出てくるエネルギー波の中で、人造人間13号が放つS.S.デッドリーボンバーとピッコロの魔貫光殺砲の2つが単体でのビジュアルに最も富んでいるものだと思っています。

コードと素材一式はgithubに置いておきました。

この惑星(ほし)の半分を吹き飛ばすほどのパワーがあるのだぞ!!!

Synthを試してみる

フロントエンド

先日東京Node学園14時限目に参加してきました。そこで@pchwさんから紹介があったSynthを試してみました。試してみた分かったことを書いておきます。

SynthはSPA作成を念頭において設計されたWebフレームワークです。

Angular.jsでサイトを作成した場合、HTMLの基本構造だけロードして中のデータは後からレンダリングされます。それだと検索エンジンフレンドリーではないというのと、APIロード完了までに空のdivやtableが表示されてしまってしまうという課題があったので、その課題に対するアプローチの1つということです。

Synthを使用しない(Angular.jsだけで実装した)場合と使用した場合のソースをさらしておきます。ちょくちょく出てくるRGBというプリフィックスやgridといったワードはプライベートプロジェクト由来のものなので特に他意はありません。

Synthを使用しない場合

テンプレート

<html ng-app>
    <head>
        <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
        <script src="angular.min.js"></script>
        <script src="05.js"></script>
    </head>
    <body ng-controller="RgbController">
        <table class="table">
            <tr>
                <th>Id</th>
                <th>Name</th>
                <th>Platform</th>
                <th>Comment</th>
                <th>IsCold</th>
            </tr>
            <tr ng-repeat="rgb in rgbjson">
                <td>{{rgb.id}}</td>
                <td>{{rgb.name}}</td>
                <td>{{rgb.platform}}</td>
                <td>{{rgb.comment}}</td>
                <td>{{rgb.is_cold}}</td>
            </tr>
        </table>
    </body>
</html>

コントローラー側です。

function RgbController($scope, $http, $timeout) {
    var uri = 'http://192.168.3.2/rgb/rgb.json';
    var param = {num: 200};

    $http.get(uri, param)
         .success(function(data, status, headers, config) {
             log('success', data, status, headers, config);
         })
         .error(function(data, status, headers, config) {
             log('error', data, status, headers, config);
         });

    // jsonを表示する
    function log(type, data, status, headers, config) {
        // delay
        $timeout(function(){
                    $scope.rgbjson = data;
                    }, 5000);
        //$scope.rgbjson = data;
    }
}

Synthを使用した場合

サーバ側。チュートリアルのコードをベースにしています。Promiseオブジェクトを返す必要があるのでmysql-promiseをnpm installしました。

back/resources/rgb/getRgb.js

exports.getIndex = function (req, res) {

    req.db.configure({
        "host": "localhost",
        "user": "xxxx",
        "password": "xxxxxxxx",
        "database": "rgb"
    }); 

    return req.db.query('SELECT * FROM grid_image').then(function (rows) {
        console.log(rows[0]);
        return { data:rows };
    }); 
};

クライアント側。コントローラーから

front/controller/rgb.js

angular.module('my_app')
.controller('rgbController', function ($scope, $http, data) {
    //console.log(data.data[0]);
    $scope.rgb_entries = data.data[0];
});

続いてビュー

front/html/rgb/getIndex.jade

ul.rgb-timeline
    li.grid(ng-repeat="entry in rgb_entries").content {{entry.id}} {{entry.name}} {{entry.comment}}

総括

私はVirtualBox仮想マシン上に環境を作りましたが、ファイルサイズがデフォルトの8GBだとチュートリアルで使用するmongodbにデータを作るgenerateTeweet.jsがディスク容量不足で失敗しました。VMで試す場合はディスクサイズを余裕を持った値にしておくと詰まずに済みます。ちなみにVMのファイルサイズの変更はこちらを参考に行いました。

preloadを試すときはAPIの返り値をPromiseオブジェクトにしないといけませんが、現時点(2014年8月24日)で公式サイトで提供しているNodeはv0.10なのでPromiseをサポートしていません。

ならばと思いgithubのmasterブランチにのっているものをビルドすると-preが付いているバージョンはnode-sassモジュールをインストールできないと怒られ、Synthサーバを起動できませんでした。Promiseが公式にサポートされる(であろう)Node v0.12のリリースを待ちましょう。

Synthサーバを立ち上げている時にサーバ側のコードを変更すると変更を検知して自動でサーバを再起動してくれるのはうれしいです。

普段Expressを使っていて、ライアントサイドとサーバサイドのコードがごっちゃになっているのはいかがなものかと思っていたのでfront/とback/でディレクトリが明確に別れている構成は好感が持てます。