shimada-kの日記

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

Node.jsでPromiseを使う場合

先日、東京Node学園12限目に参加しました。そこで@jovi0608さんから紹介があったNode-v0.12に含まれる予定のPromiseを試してみました。試してみて分かったことを書いておきます。

Node-v0.12の新機能について

コールバック地獄の例としてよく挙げられる下記のようなコードのようにディレクトリ間でファイル数とファイル名に差分が無いことを確認できればあとはファイルごとのループに書き換えられるような事例ではPromiseの恩恵が受けられます。

引用元:Node.jsにPromiseが再びやって来た!

fs.readdir(dir1, function(...) {
    fs.readFile(file1, function(...) {
        hash.update(data);
        fs.readdir(dir2, function(...) {
            fs.readFile(file2, function(...) {
                ...
            });
        });
    });
});

ただしMySQLを使用した以下のようなコードはWHERE句に直前のクエリの結果を用いているので、各クエリを非同期で実行できないためコールバック関数で順番に処理せざるを得ません。つまりこれ以上ロジックを変えられないためPromiseを使っても意味がありません。まあ当然です。

connection.query('SELECT * FROM user', function(err, rows, fields) {
    if (err) throw err;

    connection.query('SELECT * FROM user_country WHERE user_id = ' + rows[0].user_id, function(err, rows, fields){
        console.log(rows);
        connection.end();
    });
});

Promiseの恩恵を受けられるのは待ち合わせる目的のみでコールバック関数を使用している場合です。コールバック処理のネストが一掃されるわけではありません。

とはいえ、ディレクトリ操作以外にも外部APIへ複数回問い合わせに行き、それぞれの結果が出そろってから何かをするようなケースだとPromiseの恩恵を受けられるはずです。

例としてtwitterAPIを使用し、自分のPOSTをリツイートしたユーザIDを調査するロジックを実装しました。

twitterAPIはリクエストを投げまくるとHTTP#429(Too Many Requests)を返してよこすのでユーザ数を5としました)

promiseが実装されているNode.jsはgithubのmasterブランチに上がっているものを使用します(2014-05-10現在)。

shimada-k@debian:~/node_pjt/sample/chat$ node -v
v0.11.14-pre
shimada-k@debian:~/node_pjt/sample/chat$ node -e 'console.log(process.versions.v8)'
3.25.30

外部APItwitterAPIを使用し、twitterのNodeモジュールはnode-twitterを使用しました。

コールバックを使用した場合

// tweet_idのツイートをリツイートしたuser_idを返す
function getRetweeter(tweet_id) {
    twit.get('/statuses/retweeters/ids.json', {'id' : tweet_id}, function(retweeters) {
        if(retweeters){
            console.log(retweeters.ids);
            return retweeters.ids;
        }
        else{
            var err = Error("[/statuses/retweeters] APIアクセスエラー");
            throw err;
        }
    }); 
}

// 自分のtweetでリツイートされたものを返す
function getRetweeted() {
    var retweeter_ids = new Array();

    twit.get('/statuses/retweets_of_me.json', {'count' : '5'}, function(tweet) {
        if(tweet){
            for(var i = 0; i < tweet.length; i ++){
                console.log(tweet[i].text + '@' + tweet[i].id_str);

                try {
                    var retweeters = getRetweeter(tweet[i].id_str);
                    retweeter_ids.push(getRetweeter(tweet[i].id_str));
                } catch(e) {
                    console.log(e);
                }   
            }   
            //console.log(tweet);
        }   
        else{
            var err = new Error("[/statuses/retweets_of_me] APIアクセスエラー");
            throw err;
        }   
    }); 

    return retweeter_ids;
}

function getRetweeters() {
    try{
        var retweeted = getRetweeted();
        console.log(retweeted);
    } catch(e) {
        console.log(e);
    }   
}

getRetweeters();

Promiseを使用した場合

// tweet_idのツイートをリツイートしたuser_idを返す
function getRetweeter(tweet_id) {
    return new Promise(function(onFulfilled, onRejected) {
        //console.log(tweet_id);
        twit.get('/statuses/retweeters/ids.json', {'id' : tweet_id}, function(retweeters) {
            if(retweeters){
                console.log(retweeters.ids);
                onFulfilled(retweeters.ids);
            }
            else{
                var err = new Error("[/statuses/retweeters] APIアクセスエラー");
                onRejected(err);
            }
        });
    });
}

// 自分のtweetでリツイートされたものを返す
function getRetweeted() {
    var ids = new Array();
    return new Promise(function(onFulfilled, onRejected) {
        twit.get('/statuses/retweets_of_me.json', {'count' : '5'}, function(tweet) {
            if(tweet){
                for(var i = 0; i < tweet.length; i ++){
                    console.log(tweet[i].text + '@' + tweet[i].id_str);
                    ids.push(tweet[i].id_str);
                }   
                //console.log(tweet);
                onFulfilled(ids);
            }   
            else{
                var err = new Error("[/statuses/retweets_of_me] APIアクセスエラー");
                onRejected(err);
            }   
        }); 
    }); 
}

function getRetweeters() {
    return getRetweeted().then(function(ids) {
        return Promise.all(ids.map(function(id) {
            return getRetweeter(id);
        }));
    }).then(
        function(x) {
            console.log(x);
        },  
        function(err) { // all case of errors
            console.log(err.messages);
        }   
    );  
}

getRetweeters();

エラーの捕捉箇所が一元化されて大分見通しが良くなりました。

imlib2のdraw_textで遊んでみる

Rubyの画像処理ライブラリimlib2で画像に文字を埋め込んで遊んでみました。

環境

入力した文字列の文字数によって適切にフォントのサイズと縦横のマージンを計算し、さらに一行におさまる長さで改行を挟んで画像内に表示するコードを書いてみました。

Gist

フォントの大きさの単位はpt(ポイント)指定ですので、フォントサイズを決める時は画像の大きさとの兼ね合いでpx(ピクセル)からptへ変換する必要があります。

また、LinuxはDPIが96なのでpxで計算した値をフォントサイズとすると画像からはみ出してしまいます。

(imlibのコンパイルオプションで決まっているかX11の設定をAPI経由で引っ張ってきているかだと予想しています)

全角・半角が混在してる文字列を解析するのはアルゴリズムが複雑になるので、ASCII文字は全角化します。そのためgemでmojiをインストールしておきます。インストールの仕方はこちらを参照

今回は単色青色の240x240の画像に白い文字を書いてみました。

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

使用フォント:/usr/share/fonts/truetype/ttf-japanese-gothic.ttf

全角化してるためASCII文字はなんか間延びしてる印象。

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

文字数が多てもサイズとマージンが最適化されます。

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

フォントサイズはpt指定ですが、draw_textの引数で渡す座標はpx指定なので注意が必要です。

img.draw_text(font,
    str_entry,
    stringAndSize['offset_horizon'],
    stringAndSize['offset_vertical'] + pt2px((stringAndSize['margin_vertical'] + stringAndSize['size']) * index),
    color)

フォントのパスを追加すればwebからダウンロードしたフォントも使用できます。パスはttfファイルが直下に存在するディレクトリを指定すればOK

# フォントのパス追加
Imlib2::Font.add_path('/home/shimada-k/Development/fonts')
Imlib2::Font.add_path('/home/shimada-k/Development/fonts/mplus-TESTFLIGHT-058')

今回は以下のフォントを使ってみました。

自由の翼フォント(JiyunoTsubasa)

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

うつくし明朝体(UtsukushiMincho)

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

うずらフォント(uzura)

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

フロップデザインフォント(FLOPDesignFont)

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

M+ FONTS(mplus-TESTFLIGHT)

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

ロゴたいぷゴシック(07LogoTypeGothic7)

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

ロゴたいぷゴシックが一番おさまりがいい。

MySQLでインデックスが効いてないとつまりどうなるのか

MySQLでインデックスを使用しないクエリを投げると実際遅いことは分かるんですけど、マジックワード感があるのでなぜ遅くなるのか考えてみました。

すごく大きなテーブルを作ってインデックスを使用したSELECTとそうでないSELECTを投げてperfで調査しました。

環境

  • OS:Debian Squeeze (Kernel 2.6.32-5-amd64)
  • H/W:Corei5-3470T, Memory 16GB, SSD 120GB
  • perf:0.0.2.PERF

perfをインストールします

apt-get install google-perftools linux-tools-2.6.32

これでperfが動く環境になりました。次に実験用のテーブルを作りガツっとデータをINSERTします。今回使用するテーブルは下記のような構造にしました。1ユーザごとに100件のレコードを保持していることを想定して、100万ユーザ分のデータをINSERTすることにします。

mysql> DESC owning_muffins;
+------------+---------------------+------+-----+---------+----------------+
| Field      | Type                | Null | Key | Default | Extra          |
+------------+---------------------+------+-----+---------+----------------+
| id         | bigint(20) unsigned | NO   | PRI | NULL    | auto_increment |
| user_id    | bigint(20) unsigned | NO   |     | NULL    |                |
| flavor_id  | int(11)             | NO   |     | NULL    |                |
| blueberry  | tinyint(4)          | YES  |     | NULL    |                |
| strawberry | tinyint(4)          | YES  |     | NULL    |                |
| chocochip  | tinyint(4)          | YES  |     | NULL    |                |
| nuts       | tinyint(4)          | YES  |     | NULL    |                |
| created_at | datetime            | YES  |     | NULL    |                |
+------------+---------------------+------+-----+---------+----------------+
8 rows in set (0.00 sec)

INSERTするバッチ

require 'rubygems'
require 'mysql'

# insertData.rb
# データを大量にinsertするコード
# テーブルはユーザごとに100のデータを持っていることを想定
# ユーザは100万人とする
# insertするデータ数は100万×100で100000000

def insertData
    con = Mysql::connect('host', 'user', 'password', 'db_name')
    sql = 'SET NAMES utf8'
    con.query sql

    for user_id in 1..1000000 do
        for has_muffin in 1..100 do
            query_string = 'INSERT INTO owning_muffins (user_id, flavor_id, blueberry, strawberry, chocochip, nuts, created_at) values(' << user_id.to_s  << ',' << rand(10).to_s << ',' << rand(2).to_s << ',' << rand(2).to_s << ',' << rand(2).to_s << ',' << rand(2).to_s << ',\'' << Time.now.strftime("%Y-%m-%d %H:%M:%S") << '\');'
            # puts query_string
            con.query query_string
        end
    end
end

insertData()

バッチ終了後には100×100万で1億レコードのテーブルになります。

#ちなみにこのINSERTバッチ、実行完了まで丸2日かかりましたw

これでデータが入ったのでSELECTするバッチを実行してperfで計測します。

require 'rubygems'
require 'mysql'

# fetchData.rb
# データをselectするコード
# インデックスを効かせたselectか効かないselectか選択する
# 引数付きで実行するとインデックスを使用しない検索を行う

def fetchData(has_index = true)
    con = Mysql::connect('host', 'user', 'password', 'db_name')
    sql = 'SET NAMES utf8'
    con.query sql

    id = rand(100000000)
    user_id = rand(1000000)

    if has_index then
        ids = ''
        for id in 1..100 do
            if id == 100 then
                ids <<  (user_id * 100 + id).to_s
            else
                ids <<  (user_id * 100 + id).to_s << ','
            end
        end
        # puts ids
        query_string = 'SELECT * FROM owning_muffins WHERE id in (' << ids << ')'
    else
        query_string = 'SELECT * FROM owning_muffins WHERE user_id = ' << user_id.to_s
    end

    #puts query_string
    data = con.query query_string

    ret_array = Array.new

    while(record = data.fetch_hash())
        ret_array.push(record)
    end 

    # 標準出力に表示する
    # for i in 0..ret_array.length - 1 do
    #  puts ret_array[i]['id']
    # end
end

# 引数があればインデックスを無効にする
if ARGV[0] then
    has_index = false
else
    has_index = true
end

fetchData(has_index)

インデックスを使用したクエリをたたくものとインデックスをしないクエリをたたく2種類の計測を行いました。

MySQLがどんな風に子プロセスor子スレッドを作ってるか分からないのでシステム全体でイベントを計測します(-aオプションを使用する場合はrootにならないとダメです)。さらに誤差を考えてそれぞれ10回ずつ実行した平均をとります。計測コマンドは下記の通り。

# インデックスを使用した場合
perf stat -a -r 10 -e cpu-clock,task-clock,page-faults,cpu-migrations,L1-dcache-load-misses,L1-dcache-store-misses,L1-dcache-prefetch-misses,L1-icache-load-misses,L1-icache-prefetch-misses,LLC-load-misses,LLC-store-misses,LLC-prefetch-misses ruby fetchData.rb

# インデックスを使用しない場合
perf stat -a -r 10 -e cpu-clock,task-clock,page-faults,cpu-migrations,L1-dcache-load-misses,L1-dcache-store-misses,L1-dcache-prefetch-misses,L1-icache-load-misses,L1-icache-prefetch-misses,LLC-load-misses,LLC-store-misses,LLC-prefetch-misses ruby fetchData.rb hoge

計測結果です。まずはインデックスが効いている場合

score event
229.827713 cpu-clock-msecs
229.937963 task-clock-msecs
2676 page-faults
3 CPU-migrations
34293 L1-dcache-load-misses
1799415 L1-dcache-store-misses
559063 L1-dcache-prefetch-misses
1103446 L1-icache-load-misses
not counted L1-icache-prefetch-misses
1074416 LLC-load-misses
123611 LLC-store-misses
444980 LLC-prefetch-misses

実行にかかった時間は0.057323544秒でした(10回の平均値)

次はインデックスが効いていない場合

score event
115113.325665 cpu-clock-msecs
115114.371156 task-clock-msecs
4654 page-faults
40 CPU-migrations
11365832 L1-dcache-load-misses
1050214193 L1-dcache-store-misses
211685824 L1-dcache-prefetch-misses
53432908 L1-icache-load-misses
not counted L1-icache-prefetch-misses
164818024 LLC-load-misses
171005281 LLC-store-misses
119857977 LLC-prefetch-misses

実行にかかった時間は28.792978184秒でした(10回の平均値)

実行時間はおおよそ502倍になっています。最初io-waitが頻発してるのかと思っていましたがそうではなさそうです。cpu-clockと総実行時間から逆算したio-waitの時間が0に近いので他の要因であると考えます。

実行時間が502倍なのでインデックスが効いている場合の各値を502倍してインデックスが効いていない場合の数値と比較してみました。

#各数値が実行時間と正比例する前提の方法論なので乱暴ですが俯瞰して比較する際の指針にはなりえるかも、と考えました。

index_not_use / (502 * index_use) event
0.9971710235 cpu-clock-msecs
0.996701955 task-clock-msecs
0.003462475541 page-faults
0.02654514983 CPU-migrations
0.6598454059 L1-dcache-load-misses
1.161964851 L1-dcache-store-misses
0.7538370338 L1-dcache-prefetch-misses
0.09640602364 L1-icache-load-misses
not counted L1-icache-prefetch-misses
0.3054067844 LLC-load-misses
2.754221392 LLC-store-misses
0.5362569033 LLC-prefetch-misses

L1とLLC(ラスト・レベル・キャッシュ。今回使用したマシンは3次までCPUキャッシュがあるのでL3キャッシュのこと)のSTOREキャッシュミスが比較的多いことが分かります。なのでデータをキャッシングしておく際のオーバーヘッドが大きいようです。

  1. キャッシュを参照
  2. キャッシュに無い
  3. メモリか他のキャッシュからデータを取ってきて自分の所に保存

これを都度繰り返しているということになります。フルスキャンなのでそのようになるのでしょう。

#なぜL1キャッシュよりL3キャッシュのミスの方が割合が多いのかというのはプロセスマイグレーションの話とかCPUキャッシュのウェイ数とかが関係してきそうなのでこれ以上深追いはしたくない。

twitterの投稿画面風UIの作り方

iPhonetwitterアプリのようなアップロードUIを作ってみました。

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

こういうやつです。上記はtwittertweet入力画面です。

テキストを入力する場所があって、その下にキーボードがある。さらにその間にツールバーのような領域があってボタンを配置できる。というもの。twitterの場合は位置情報とカメラとフォトライブラリの3つのボタンがあります。

調べていくとUITextViewのAccessoryViewを使えば実現できることが分かりました。

作ってみたものはSingleViewApplicationをベースにNavigationControllerを入れて初期画面のWriteボタンをタップしたら入力用の画面へ遷移する。遷移先にはUITextViewと、UIViewにラップされたツールバーがあってツールバーにカメラボタンが含まれているというものです。

#カメラボタンを押した時の挙動は実装してないのでログしかでません。。

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

#UITextViewに表示されている英文はデフォの文章です

AccessoryView用のUIViewはInterfaceBuilderのDockエリアに置くことです。実験した環境ではDockエリアでは無いところに置くとキーボードの下に隠れてしまいました。

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

InterfaceBuilder上の親子関係はこんな感じ。

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

ちなみにchatworkのモバイルアプリでも同じようなUIが使われてます。わりと一般的なUIのはずなのに日本語で解説してるサイトが少なすぎる。

Dockエリアに置いている状態だとInterfaceBuilder上で見た目が確認できないのでAccessoryViewを弄るときは一度どこかのサブビューに置いてからの方がよいと思います。

ソース全文

//
//  MUInputViewController.m
//  modernUpload
//
//  Created by 島田克弥 on 2014/03/15.
//  Copyright (c) 2014年 shimada-k. All rights reserved.
//

#import "MUInputViewController.h"

@interface MUInputViewController ()
@property (weak, nonatomic) IBOutlet UITextView *sendTextView;
@property (strong, nonatomic) IBOutlet UIView *accessoryView;

- (IBAction)startCamera:(UIBarButtonItem *)sender;
- (IBAction)writeDone:(UIBarButtonItem *)sender;

@end

@implementation MUInputViewController

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    
    // 入力状態にする
    [self editAction:self];
}


- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    _sendTextView.delegate = self;
    // キーボードが出てくる時の通知を受け取るよう設定
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(keyboardWillShow:)
                                                 name:UIKeyboardWillShowNotification
                                               object:nil];
    // キーボードが閉じらる時の通知を受け取るよう設定
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(keyboardWillHide:)
                                                 name:UIKeyboardWillHideNotification
                                               object:nil];
    
}

- (IBAction)writeDone:(UIBarButtonItem *)sender {
    [_sendTextView resignFirstResponder];
}

- (void)editAction:(id)sender {
    [_sendTextView becomeFirstResponder];
}

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

#pragma mark - Text view delegate methods
- (BOOL)textViewShouldBeginEditing:(UITextView *)textView{
    
    if (_sendTextView.inputAccessoryView == nil) {
        _sendTextView.inputAccessoryView = _accessoryView;
    }
    
    return YES;
}

- (BOOL)textViewShouldEndEditing:(UITextView *)textView {

    [textView resignFirstResponder];
    
    return YES;
}

#pragma mark - Responding to keyboard events
- (void)keyboardWillShow:(NSNotification *)notification {

    NSDictionary *userInfo = [notification userInfo];
    
    // キーボードが表示完了後の場所と大きさを取得する
    NSValue *aValue = [userInfo objectForKey:UIKeyboardFrameEndUserInfoKey];

    CGRect keyboardRect = [aValue CGRectValue];
    keyboardRect = [self.view convertRect:keyboardRect fromView:nil];
    
    CGFloat keyboardTop = keyboardRect.origin.y;
    CGRect newTextViewFrame = self.view.bounds;
    
    // キーボードの大きさに応じてテキスト表示領域を再計算する
    newTextViewFrame.size.height = keyboardTop - self.view.bounds.origin.y;
    
    // キーボードのアニメーション時間を取得する
    NSValue *animationDurationValue = [userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey];
    NSTimeInterval animationDuration;
    [animationDurationValue getValue:&animationDuration];
    
    // アニメーション実行準備
    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:animationDuration];
    
    // UITextViewの大きさを変更する
    _sendTextView.frame = newTextViewFrame;
    
    // アニメーションの開始
    [UIView commitAnimations];
}

- (void)keyboardWillHide:(NSNotification *)notification {
    
    NSDictionary *userInfo = [notification userInfo];

    // キーボードのアニメーション時間を取得する
    NSValue *animationDurationValue = [userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey];
    NSTimeInterval animationDuration;
    [animationDurationValue getValue:&animationDuration];
    
    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:animationDuration];
    
    // UITextViewの大きさを元に戻す
    _sendTextView.frame = self.view.bounds;
    
    // アニメーションの開始
    [UIView commitAnimations];
}


/*
#pragma mark - Navigation

// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    // Get the new view controller using [segue destinationViewController].
    // Pass the selected object to the new view controller.
}
*/

- (IBAction)startCamera:(UIBarButtonItem *)sender {
    NSLog(@"写真がとれたらいいね");
}

@end

参考サイト

KeyboardAccessory

Node.jsでサーバ間通信

Node.jsのAPI内で別サーバへ問い合わせてその内容をejsへ渡すということをやってみました。リアルタイム性を持たせたいからNode.js/websocketなんだけども、処理全部をjsで書くのはなーという考えからきてます。

    var options = { 
        host : 'localhost',
        // ポート番号
        port : 8001,
        // APIのパス
        path : '/hoge?foo=' + bar, 
    };  

    http.get(options, function(resp) {
        var content = ""; 

        resp.on('data', function(chunk){
            content += chunk;
        }).on('end', function(){
            data = JSON.parse(content);
            console.log(data);

            res.render('index.ejs', {
                layout: false,
                locals: {server_data : data}
            }); 
        }); 
    }).on('error', function(e) {
        console.log("Got error: " + e.message);
    });

Nodeサーバは80番で動かして8001番でApacheを動かしてsinatraでバックエンドを構築しているという環境です。物理的には同じ筐体内で完結しています。

バックエンドはsinatraRubyで書くなら以下のような形式になります。

get '/hoge' do
    #
    # 何かの処理
    #
    JSON.generate(data)
end

JSON.generateしているのはNode側へ渡すときにjson形式にしているからです。json形式に縛られる必要は特にないです。

http-proxyモジュールなどを経由してNodeサーバを動かす場合はルーターの設定を工夫すれば外部からバックエンドのAPIへNode経由ではなく直接アクセスさせることも可能です。

var httpProxy = require('/usr/local/lib/node_modules/http-proxy');

var options = {
  //hostnameOnly: true,

  router: {
    'サーバ名/hoge'    :'localhost:8001/hoge',
  }
};

var prodServer = httpProxy.createServer(options);
prodServer.listen(80);
console.log('prodServer works');

リアルタイム部分はNode.jsで書いてバックエンドは使い慣れた処理系で、といったシチュレーションは結構あるような気がします。

Interface Builderで置いたBarButtonItemにアイコン画像を設定する

UIBarButtonItemのアイコンを独自のものに差し替えたい場合の手段です。若干はまったのでメモ書き。下の画像のように2つのBarButtonItemが配置されているとします。

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

facebookのいいね!の画像をアイコンとして設定することとします。

like40x36.png
f:id:shimada-k:20140308232728p:plain

コードから指定する場合
    UIImage *image = [UIImage imageNamed:@"like40x36.png"];
    [_item1 setImage:image];

僕はsetImageではなくsetBackgroundImageを使用してたためBarButtonItem.titleが表示されてしまい途方にくれていました。setBackgroundImageではなくsetImageを使用します。

Interface Builderから指定する場合

対象のUIBarButtonItemオブジェクトを選択した状態でimageを指定します。画像ファイルがプロジェクトに正しく追加されていれば右側の矢印ボタンから選択できます。ここではitem2の方の画像をいいね画像に差し替えます。

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

ここまでの状態でInterface Builder上では下のような見え方になります。

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

実行すると両方いいね!の画像にアイコン画像が変更されているはずです。

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

コードからアイコン画像を指定する場合はBarButtonItemをアウトレット接続しないといけないのでInterface Builderから指定する方法の方がスマートだと思います。

ソース全文

//
//  BBIViewController.m
//  barButtonIcon
//
//  Created by 島田克弥 on 2014/03/08.
//  Copyright (c) 2014年 shimada-k. All rights reserved.
//

#import "BBIViewController.h"

@interface BBIViewController ()
@property (weak, nonatomic) IBOutlet UIBarButtonItem *item1;
- (IBAction)tabItem1:(UIBarButtonItem *)sender;
- (IBAction)tabItem2:(UIBarButtonItem *)sender;

@end

@implementation BBIViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
	// Do any additional setup after loading the view, typically from a nib.
    UIImage *image = [UIImage imageNamed:@"like40x36.png"];
    [_item1 setImage:image];
}

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

- (IBAction)tabItem1:(UIBarButtonItem *)sender {
    NSLog(@"item1が押されたよ");
}

- (IBAction)tabItem2:(UIBarButtonItem *)sender {
    NSLog(@"item2が押されたよ");
}
@end

CollectionViewとWebViewで画像ビューワを作ってみる

せっかくiOSの開発環境が手元にあるので試しにアプリを作ってみました。
webの画像をCollectionViewで表示する画像ビューワです。

Googleのロゴ画像を出しています。

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

WebViewでロゴ画像のURLを指定してCollectionViewで並べてるだけです。

まずCollectionViewを追加します。ControllerではなくViewのほう。

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

次にCellの中にWebViewを追加します。

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

親子関係はこんな感じ。

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

CollectionViewのセルにIDを振っておきます。ソースからアクセスするためです。

WebViewにタグを付けておきます。これもソースからアクセスするためです。

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

最後にCollectionViewをソースとアウトレット接続します。mファイルに接続しておきます。

これでIBの操作は終わり。あとはソースを書いていきます。

データソースの指定をやります。viewDidLoadの中。

    [_ViewerContents setDataSource:(id)self];

numberOfSectionsInCollectionViewとnumberOfItemsInSectionメソッドを追加します。セクションは1つだけで今回はセルの数を36とか適当に指定しておきます。

- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
    //セクションは1つ
    return 1;
}

// セルの個数を返すメソッド
-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
    return 36;
}

cellForItemAtIndexPathを追加します。こいつはセルごとに呼び出されるメソッドです。今回はGoogleのロゴ画像を36個分、同じものを表示するので、indexPath.rowでごにょごにょする必要なし。

ここまでで先ほどのアプリが完成。ただし「Google」のGの上の方しか表示されてないのでIBでWebViewを選択して「Scales Page To Fit」にチェックを入れておきます。これで画像ファイルの大きさがWebViewのサイズにあわせて自動的にスケールしてくれます。

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

これで縦のサイズはWebViewにスケールしてくれました。まあ、Googleは横長なんでしょうがないってことで。

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

ソース全文
IVViewController.m

//
//  IVViewController.m
//  ImageViewer
//
//  Created by 島田克弥 on 2014/02/15.
//  Copyright (c) 2014年 shimada-k. All rights reserved.
//

#import "IVViewController.h"

@interface IVViewController ()
@property (weak, nonatomic) IBOutlet UICollectionView *ViewerContents;

@end

@implementation IVViewController

- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
    //セクションは1つ
    return 1;
}

// セルの個数を返すメソッド
-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
    return 36;
}

//Method to create cell at index path
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
    
    NSLog(@"セル番号だよ index_path.row:%ld", indexPath.row);
    UICollectionViewCell *cell;
    
    cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cell1"forIndexPath:indexPath];
    cell.backgroundColor = [UIColor blackColor];
    
    UIWebView *image = (UIWebView *)[cell viewWithTag:1];
    
    NSString *str_url = @"https://www.google.co.jp/images/srpr/logo11w.png";
    NSURL * url = [NSURL URLWithString:str_url];
    NSURLRequest *urlReq = [NSURLRequest requestWithURL:url];
    [image loadRequest:urlReq];
    
    return cell;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
	// Do any additional setup after loading the view, typically from a nib.
    [_ViewerContents setDataSource:(id)self];
}

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

@end

参考サイト
Collection Viewの使い方