はりまや日記

なんか適当にいろいろ綴ったりなんかするところ

Q. CocoaLumberjack で色が出ないときやること

A. 環境変数 XcodeColors が設定されているか確認しましょう

その経緯

ここ を参考にやっていたのだが、どうにも色が出ない。
NSLog で色指定すればカラーになるのだが、CocoaLumberjack 経由だとだめ。

んで、この辺 でなんか環境変数ってあったなー、と思って試してみるとビンゴでした。
よく見ると公式 にも書いてあった…

あと、リリース時にカラーになっちゃうと駄目らしいんで、デバッグ時のみ環境変数を仕込めばいいかな。
こんな感じ

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
#ifdef DEBUG
    setenv("XcodeColors", "YES", 0);
#endif
    [DDLog addLogger:[DDTTYLogger sharedInstance]];
    [[DDTTYLogger sharedInstance] setColorsEnabled:YES];
}

iphoneアプリ開発日記その11

webview の座標を取ってみる

webview にポップアップを出したくなったので、タッチしたところの座標が必要なのです。

タッチイベントの検知は前回やったので、あとは座標を取ればいいだけかな。
ひゃっはーこいつは楽勝だぜ!


いったいどうやって座標を取れというのです?

グーグル先生にお尋ねしたところ、3つ候補が返ってきた

  1. ios の GestureRecognizer で取得する
  2. ios の touchesBegan メソッドで取得する
  3. javascript で取得する

なんとなくジェスチャーは使いたくないので、2か3だな。
ひとまず両方やってみよう。


touchesBegan で座標を取る

さて、試してみよう。
単純に touchesBegan メソッドを実装すればいいのかと思ったら、webview の場合はそのままでは動かないようだ。
いろいろ見ていたところ、カテゴリを使ってイベントを渡してあげるといいみたい。
ここを参考にした。

んで、コードがこうなった。
後で使う為に、touchesBegan 内で座標をプロパティに突っ込んでおく。

コード

UIScrollView+TouchEvent.h

#import <UIKit/UIKit.h>

@interface UIScrollView (TouchEvent)

@end


UIScrollView+TouchEvent.m

#import "UIScrollView+TouchEvent.h"

@implementation UIScrollView (TouchEvent)

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [[self nextResponder] touchesBegan:touches withEvent:event];
}

@end

viewController.m

@interface ViewController ()
@property CGPoint touchPoint;
@end

@implementation ViewController
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    self.touchPoint = [[touches anyObject] locationInView:self.view];
    NSLog(@"point: %f, %f", self.touchPoint.x, self.touchPoint.y);
    [super touchesBegan:touches withEvent:event];
}
@end

結果 ちゃんと座標が取れました。

2013-06-25 11:48:33.319 webTest[80946:11303] point: 171.000000, 145.000000

OK っすね


javascript で座標を取る


前回、タッチイベントを扱ったのですが、
それに「pageX/pageY」というプロパティがありました。
これを使えばいけるだろうか…

ためしてみましょう。


コード

  $('body').bind("touchstart",
                 function()
                 {
                 event.preventDefault;
                 alert("pageX:"+event.pageX+" pageY: " + event.pageY);
                 });

結果
iphone シミュレータの高さは450 程度なのに、普通に pageY に 700 とかくる。
pageX/pageY は、やはりページ全体の座標だった。


さて、どうするか。
ここを見ると、clientX/clientY プロパティが使えそうだ。
しかし、touchevent には含まれていないようだ。

調べてみると、event.changedTouches[0] の中に入ってるみたい。
iPhone/Android/PC 対応。jQuery で書くタッチイベント (フェンリル | デベロッパーズブログ)

試してみる


コード

$(function()
  {
  var bodyInterval = 300;
  $('body').bind("touchstart",
                 function()
                 {
                 event.preventDefault;
                 
                 var x = event.changedTouches[0].clientX;
                 var y = event.changedTouches[0].clientY;
                 
                 timer = setTimeout(function(){
                                    alert("x:"+x+" y: " +y);
                                    }, bodyInterval);
                 });
  
  $('body').bind("touchend touchmove touchcancel",
                 function(){ clearTimeout(timer) ;
                 });
});

結果
タッチしたのは両方とも左上らへんです。

スクロール前
f:id:harrymaya:20130625141918p:plain

スクロール後
f:id:harrymaya:20130625141924p:plain


これで、ページのスクロールに関係なく、画面上の座標が取得できました。

iphoneアプリ開発日記その10

webview のリンク長押しをなんとかする 二日目

さて、前回までで リンク長押しのイベント取得と、その結果を native に返すことができた。
今日は、リンク URL を取得してみよう。


URL 取得

そもそも javascript をほとんどやってないので、どうやって取るのかイメージがさっぱりです。
ググっていると、参考になりそうなページをいくつか見つけた。

jQuery.Event - jQuery 日本語リファレンス
iPhone/Android/PC 対応。jQuery で書くタッチイベント (フェンリル | デベロッパーズブログ)

ほう、event.changedTouches[0].pageX で座標が取れるのか。
試してみよう

$( document ).ready( function()
		{
		var interval = 500;
		$( "a" ).bind( "touchstart", function()
			{
			timer = setTimeout(function()
				{
                                window.location = 'nativecode://touchstart/hoge' + event.changedTouches[0].pageX;
				}, interval);
			function clearFunction(){
			clearTimeout(timer);
			alert("touch cancel!");
			}
			$( "a" ).bind( "touchend touchmove touchcancel", clearFunction );
			});
		});

... 動かない。いやもう全然。
というかタップ自体認識しなくなってる。

んで、いろいろ試していたところ、上の階層なら event を取れることが判明

$( document ).ready( function()
		{
		var interval = 500;
                // この辺は event 生きてる
                alert(event.currentTarget);
		$( "a" ).bind( "touchstart", function()
			{
			timer = setTimeout(function()
				{
                // この辺はだめ
                                //alert(event.currentTarget);
                                window.location = 'nativecode://touchstart/hoge' + event.changedTouches[0].pageX;
				}, interval);
			function clearFunction(){
			clearTimeout(timer);
			alert("touch cancel!");
			}
			$( "a" ).bind( "touchend touchmove touchcancel", clearFunction );
			});
		});

んー、スコープの問題なのかなー
いろいろ試したところ、上の階層で変数に格納しておけば event は生きてるが、プロパティにアクセスできない。

$( document ).ready( function()
		{
		var interval = 500;
                var target = event.currentTarget;
		$( "a" ).bind( "touchstart", function()
			{
			timer = setTimeout(function()
				{
                                alert(target);//これは動く
                                //alert(target.type);//動かない
				}, interval);
//略

で、ここを見ていたら、こんな記述があった。

var target = $(event.target);

これでやってみると、target.type にアクセスができた。undefined だったけど。
一体これはなんなのだろう?


$(event.target) とはなんだったのか

ということで、$() について調べてみた
どうやら、「jqueryオブジェクト」という物のようだ。

このへんを参考にすると、
javascript のオブジェクトは、メソッドとプロパティを持っていて、
$() で記述すると、それは jquery のオブジェクトとして扱われると。
で、jquery のオブジェクトということがわかっているので、そのメソッドとプロパティにアクセス出来るようになる。

ざっくりこんな理解でいいのかな。
void型のポインタにキャストかました感じっぽいな。


タッチイベントから、 url と タイトルを取得する

まあ、そんなこんなでイベントのプロパティにアクセス出来るようになりました。

早速取得してみましょう

$( function()
	{
	var interval = 500;
        var target = $(event.currentTarget);
        var title = target.text();
        var url = target.attr('href')
 
	$( "a" ).bind( "touchstart", function()
		{
		timer = setTimeout(function()
			{
                        window.location = 'nativecode://touchstartxxx/' +url + title;
			}, interval);
//略

結果

2013-06-20 22:04:34.959 harafuwa[58944:11303]              url: nativecode://touchstartxxx/http://pad-soku.com/archives/29571047.html%20%20%20%E3%80%90%E3%83%91%E3%82%BA%E3%83%89%E3%83%A9%E3%80%91%E5%87%84%E3%81%99%E3%81%8E%E3%81%A6%E3%83%AF%E3%83%AD%E3%82%BF%EF%BD%97%EF%BD%97%E3%83%89%E3%83%AD%E3%83%83%E3%83%97%E3%81%A7%E5%A3%81%E7%B4%99%E3%82%92%E4%BD%9C%E3%82%8B%E8%81%B7%E4%BA%BA%E3%81%8C%E3%83%A4%E3%83%90%E3%81%99%E3%81%8E%E3%82%8B%EF%BD%97%EF%BD%97%EF%BD%97%20%E3%83%A1%E3%82%BF%E3%83%89%E3%83%A9%E9%80%9F%E5%A0%B1%20-%202013-06-20%2021:57:48
2013-06-20 22:04:34.960 harafuwa[58944:11303]           scheme: nativecode
2013-06-20 22:04:34.960 harafuwa[58944:11303]             host: touchstartxxx
2013-06-20 22:04:34.961 harafuwa[58944:11303]             path: /http://pad-soku.com/archives/29571047.html   【パズドラ】凄すぎてワロタwwドロップで壁紙を作る職人がヤバすぎるwww メタドラ速報 - 2013-06-20 21:57:48

取れた … けど文字多いなー
URLの文字数制限って4000文字くらいだっけ? まあ大丈夫か。

とりあえず取得できました。


ユニークなアプローチ

URL に長い文字列をくっつけるのがちょっと気持ち悪いので、違うやり方を考えてみる。

  • 案1 url をキーに要素を検索する ← url が被る可能性があるのでアウト
  • 案2 ページ内の要素をユニークに判別できるものを利用する(あれば)

という訳で、ユニークな識別子を探してみた。
index(subject) - jQuery 日本語リファレンス

これならページ内に複数あっても、一意に判別できるようだ。

考え方としては、ページ内の全aタグに対して、ターゲットが何番目なのかが知りたいわけだ。
するとこうなるか

var hoge = $('a').index(target)

で、逆にインデックスから要素にアクセスするのが eq
eq(index) - jQuery 日本語リファレンス

コードはこうなる

$('a').eq(index).attr('href')
$('a').eq(index).text()

あとはこれを実行すれば、それぞれ戻り値として取得できる。


感想

こうやって書くとさっくりできたような気もするが、割と試行錯誤で大変だった。
でも、ちょっと javascript できる気になれました。

iphoneアプリ開発日記その9

webview のリンク長押しをなんとかする 一日目

やりたいこと
リンク長押しを検知して、リンクのURLとタイトルを取得したい

ということでやってみる


native からアプローチ

とりあえず長押しのジェスチャーを仕込めばいいのではないか。
そういう風に考えていた時期がありました。

とりあえずやってみる
ここを参考に、以下のコードを追加

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
        
    UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
    [self.view addGestureRecognizer:longPress];
    
}
-(void)handleLongPress:(UILongPressGestureRecognizer *)gesture
{
    if (gesture.state == UIGestureRecognizerStateBegan) {
        NSLog(@"long pressed.");
    }
}

あれ? 上手くいかないな…
もう少し調べてみると、次の関数も必要なようだった
参考

- (void)viewDidLoad
{
    〜〜略

    longPress.delegate = self;
}

-(void)handleLongPress:(UILongPressGestureRecognizer *)gesture
{
    if (gesture.state == UIGestureRecognizerStateBegan) {
        NSLog(@"long pressed.");
    }
}

結果

2013-06-20 20:00:51.273 webTest[52816:11303] long pressed.

一応長押しのイベントは取れたが、リンクURLはどうすればいいのだろうか。
ここで手詰まりになった。
ググってみても、ほとんど javascript を使っているようだ。
じゃあ javascript でやってみることにしよう


javascript からアプローチ

この辺を参考にやってみる
UIWebViewのリンク長押しを簡単にハックする | エンジニア開発記
ARX Developers Blog: jQuery Mobileを使わずにスマートフォンのtapholdイベント


まず標準のロングプレスジェスチャを無効化

[self.webView stringByEvaluatingJavaScriptFromString:@"document.documentElement.style.webkitTouchCallout = 'none';"];

できた


次は関数定義か。リンクなんでaタグを取ればいいのかな?

        NSString* script = @"$( document ).ready( function(){var interval = 500;$( \"a\" ).bind( \"touchstart\", function(){timer = setTimeout(function(){alert( \"リンク長押しされたっす\" );}, interval);function clearFunction(){clearTimeout(timer);}$( \"a\" ).bind( \"touchend touchmove touchcancel\", clearFunction );});});";
        
        [self.webView stringByEvaluatingJavaScriptFromString:script];

うむ、見にくい。今後は普通にjavascript 書いて vi 辺りで置換しよう。
とりあえず動いたが、何をやっているのかよくわからない。



アラートを出してみる

$( document ).ready( function()
		{
		var interval = 500;
		$( "a" ).bind( "touchstart", function()
			{
			timer = setTimeout(function()
				{
				alert( "リンク長押しされたっす" );
				}, interval);
			function clearFunction(){
			clearTimeout(timer);
			alert("touch cancel!");
			}
			$( "a" ).bind( "touchend touchmove touchcancel", clearFunction );
			});
		});

あれ? これだと長押しとキャンセルのアラートが両方出て、おかしくなってしまった。
ふむ、touchend って辺りが怪しいな。
このへんで関数とイベントをバインドしている、ということか。



touchcancel の時は別関数にしてみたが、どうにもcancelイベントが発生しない。
今の状況では起きないのかも

//これを追加
function cancelFunction(){
	clearFunction;
	return "touch cancel!";
}
//cancel 時の処理を分離
$( "a" ).bind( "touchend touchmove", clearFunction );
$( "a" ).bind( "touchcancel", cancelFunction );


ぐぬぬ…動きがよく掴めない
ログを吐こうとしたのだが、console.log も出ないし、return やってもうまくいかない。

そうだ native 連携しよう。


javascript → native 連携をやる

とりあえずログ吐きたいだけだが、どのみちやるつもりだったしいいよね!

こことかここを参考

ふむふむ、要は javascript から URL リクエスト飛ばして、native 側で URL 読み込むときに判別する感じか。

となると、こんな感じでいけそう

timer = setTimeout(function()
{
//alert( "リンク長押しされたっす" );
window.location = 'nativecode://touchstart/hoge';
}, interval);
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
    NSURL* url = request.URL;
    if([[url scheme] isEqualToString:@"nativecode"]){
        NSLog(@"from js. request: %@", request);
        NSLog(@"             url: %@", url);
        NSLog(@"          scheme: %@", url.scheme);
        NSLog(@"            host: %@", url.host);
        NSLog(@"            path: %@", url.path);
    }
}

結果

2013-06-20 20:45:19.494 harafuwa[55203:11303] from js. request: <NSMutableURLRequest nativecode://touchstartxxx/hoge>
2013-06-20 20:45:19.494 harafuwa[55203:11303]              url: nativecode://touchstartxxx/hoge
2013-06-20 20:45:19.494 harafuwa[55203:11303]           scheme: nativecode
2013-06-20 20:45:19.495 harafuwa[55203:11303]             host: touchstartxxx
2013-06-20 20:45:19.495 harafuwa[55203:11303]             path: /hoge

これで結果を表示できる

というか今更ですが、javascript を別ファイルに書いてるので、普通に読み込んでロードすればいいですよね。
そうしましょう。

iphoneアプリ開発日記その8

webView にスクロールボタンをつけよう

その動機

最近スマホを使っていると、指が痛くなることがある。
果たして手の油が少ないのか皮膚が弱いのか

まあそんなこんなで、ボタンタップでスクロールした方が、
指に負担が少なかろうと考えました。


実現へのアプローチ

さて、ボタンスクロールをどうやって実現するのだろう。
当初は webview にそれっぽい API があるんじゃないかなーと考えていました。

実際調べてみると、どうも API なんぞ無い模様。
ただし、webview から javascript が実行できるようで、それを使ってスクロールが出来るようでした。
さらに調べてみると、webview で jquery を使えるらしい。
面白そうなんでこれにしよう


jquery を読み込む

まず、jquery 本体をダウンロード→ここ
バージョンは 1.10.1 だった。

次にここを参考にして、プロジェクトにjquery を登録

これで jquery が使えるようになった。

一応動作確認をしておく
以下のコードを、適当なボタンとかに設定して実行する

    // load jquery
    NSString *path = [[NSBundle mainBundle] pathForResource:@"jquery-1.10.1" ofType:@"js"];
    NSString* jqueryMain = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
    [_webView stringByEvaluatingJavaScriptFromString:jqueryMain];

    NSString *script = @"alert($().jquery);";
    [_webView stringByEvaluatingJavaScriptFromString:script];

結果
f:id:harrymaya:20130613042805p:plain

ちゃんと動いているようだ。


jquery のロードタイミング

さて、ひとまず jquery が動いたわけだが、jquery の本体を毎回読み込む必要はないわけで。
どこかで一回読み込めばよいだろう。


とりあえず、viewDidLoad で読み込んでみる。
… あれ? 動かない。
なんだろう。スコープの関係だろうか。


jquery 本体のコードを static にしてみた
→ 動かない。


うーむ、毎回呼ばないとだめなのだろうか。
試しに、ボタンを複数用意して、片方だけに jquery 読み込み処理を書いてみた
jquery 読み込み処理を書いたボタンを押したあとなら、書いてない方も動く


ふむ、一度読み込んでしまえばOK なわけか。
ということは、読み込むタイミングが悪いということだな。

そうなると、読み込ませるタイミングは URL 読み込みが終わってからかなー
このへんか → webViewDidFinishLoad

試してみる


@interface WebViewController ()
@property NSString* jqueryMain;
@end

- (void)viewDidLoad
{
    [super viewDidLoad];

    // いろいろ初期化処理

    // load jquery
    NSString *path = [[NSBundle mainBundle] pathForResource:@"jquery-1.10.1" ofType:@"js"];
    self.jqueryMain = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
}

- (void)webViewDidFinishLoad:(UIWebView *)webView {
    [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
    
    // load jquery
    [_webView stringByEvaluatingJavaScriptFromString:self.jqueryMain];
       
}

これで一度だけ読み込めばよくなった。


スクロール機能の実装

ここまで来たら、あとは jquery を使うだけである。
だが、jquery というか javascript はほとんどやってないので、これも調べながらになる。

このへん を参考に、どうするか考える。

見たところ、scrollTop で指定した位置に移動するようだ。
あとは以下の関数を組み合わせればよさげ

$('body').offset().top ページ全体の先頭位置
$('body').height() ページ全体の高さ
$(window).scrollTop() ウィンドウの先頭位置
$(window).height() ウィンドウの高さ

考え方としては、こんな感じ
「ページ全体の先頭位置 + 全体の高さ 」に移動すれば、一番下まで移動できる。
「ウィンドウの先頭位置 +ー ウィンドウの高さ」で移動すれば、上下に1画面分移動する

実装してみる

- (IBAction)pressUpward:(id)sender {
    NSString *script = @"$('body').animate({scrollTop:$(window).scrollTop() - $(window).height()});";
    [self execJQuery:script];
    
}

- (IBAction)pressDownward:(id)sender {

    NSString *script = @"$('body').animate({scrollTop:$(window).scrollTop() + $(window).height()});";
    [self execJQuery:script];
    
}

- (IBAction)pressPageEnd:(id)sender {
    NSString *script = @"$('body').animate({scrollTop:$('body').offset().top+$('body').height()}, 'fast')";
    [self execJQuery:script];
}

これでページスクロール機能が実装できました。
本当は、ページの上下限を考えて、オーバーするようなら0とか最大値を返そうと思っていたのですが、
特に対応しなくても問題なさそうでした。
webkitjquery 側でちゃんとやってくれているみたい。


scrollTo のセレクタについて

参考にしたページに書いてあったように、本来 scrollTo メソッドを使うときにはセレクタを指定します。
で、それを取得するのに $.browser.webkit みたいな関数? を使うのですが、こいつは jquery 1.9 で廃止されたとのこと →ここ

プラグインを入れれば使えるようですが、今回は ios オンリーなので 'body' ベタ書きで対応しときました。
クロスプラットフォームで使う気ないんでいいかなー

iphoneアプリ開発日記その7

処理の並列化を考える

下調べ

ios での並列化処理について調べてみた。
こんな感じ

名称 処理系 特徴
gcd c 最適化してくれる
NSOperation objc 最適化してくれる。gcdベース
pthread c リソース管理などを全部自前でやらなくては!
NSThread objc リソース管理などを全部自前でやらなくては!

今回は NSOperation で行こうと思います。
そんな性能いらないし。



さて、多分に漏れず NSOperation にも Blocks 化の波が来ています。
それを使いませう


使いそうな機能としてはこんなものです。

  • NSBlockOperation
    • +blockOperationWithBlock:^(void)block
      インスタンス生成時にタスクを追加する
    • -addExecutionBlock:^(void)block
      タスクを追加する
  • NSOperation
    • -setCompletionBlock^(void)block
      他のタスクが完了したときに実行する処理を追加
    • -addDependency: (NSOperation *)
      タスクの依存関係を追加
    • -start
      タスク実行

詳細はリファレンスで
ここ


で、実行はstart でも出来るのですが、NSOperationQueue を使ってもできます。
単純なタスクなら NSBlockOperation で完結してもよさそう。

実験

というわけで、operation だけのやつと Queue 付きのやつを動かしてみよう

- (void)operationOnly{

    NSBlockOperation * operation = NSBlockOperation.new;
    NSInteger sleepInterval = 2;
    
    for (int i = 0; i< 10; i++){
        
        [operation addExecutionBlock:^{
            
            [NSThread sleepForTimeInterval:sleepInterval];
            NSLog(@"wakeup. i: %d, thread: %@", i, [NSThread currentThread]);

        }];
        
    }
    [operation setCompletionBlock:^{NSLog(@"complete: %s", __func__ );}];

    [operation start];
    
    NSLog(@"done?");
    
}

- (void)operationWithQueue
{
    
    NSOperationQueue * queue = NSOperationQueue.new;
    NSBlockOperation * operation = NSBlockOperation.new;
    NSInteger sleepInterval = 2;
    
    for (int i = 0; i< 10; i++){
        
        [operation addExecutionBlock:^{
            
            [NSThread sleepForTimeInterval:sleepInterval];
            NSLog(@"wakeup. i: %d, thread: %@", i, [NSThread currentThread]);
            
        }];
        
    }
    [operation setCompletionBlock:^{NSLog(@"complete: %s", __func__ );}];

    [queue addOperation:operation];
    
//    [queue waitUntilAllOperationsAreFinished];
    
    NSLog(@"done?");
    
}

NSOperationのみの結果

2013-06-08 01:26:10.217 NSOperationLesson[1039:12303] wakeup. i: 0, thread: <NSThread: 0x71488b0>{name = (null), num = 3}
2013-06-08 01:26:10.217 NSOperationLesson[1039:12c03] wakeup. i: 1, thread: <NSThread: 0x756a300>{name = (null), num = 4}
2013-06-08 01:26:10.217 NSOperationLesson[1039:11303] wakeup. i: 2, thread: <NSThread: 0x713dc00>{name = (null), num = 1}
2013-06-08 01:26:10.217 NSOperationLesson[1039:14c03] wakeup. i: 3, thread: <NSThread: 0x7642c80>{name = (null), num = 5}
2013-06-08 01:26:12.224 NSOperationLesson[1039:14c03] wakeup. i: 7, thread: <NSThread: 0x7642c80>{name = (null), num = 5}
2013-06-08 01:26:12.224 NSOperationLesson[1039:12c03] wakeup. i: 5, thread: <NSThread: 0x756a300>{name = (null), num = 4}
2013-06-08 01:26:12.224 NSOperationLesson[1039:12303] wakeup. i: 4, thread: <NSThread: 0x71488b0>{name = (null), num = 3}
2013-06-08 01:26:12.224 NSOperationLesson[1039:11303] wakeup. i: 6, thread: <NSThread: 0x713dc00>{name = (null), num = 1}
2013-06-08 01:26:14.228 NSOperationLesson[1039:14c03] wakeup. i: 8, thread: <NSThread: 0x7642c80>{name = (null), num = 5}
2013-06-08 01:26:14.228 NSOperationLesson[1039:12c03] wakeup. i: 9, thread: <NSThread: 0x756a300>{name = (null), num = 4}
2013-06-08 01:26:14.230 NSOperationLesson[1039:11303] done?
2013-06-08 01:26:14.230 NSOperationLesson[1039:12c03] complete: __31-[ViewController operationOnly]_block_invoke_2


Queue付きの結果

2013-06-08 01:27:23.384 NSOperationLesson[1063:11303] done?
2013-06-08 01:27:25.387 NSOperationLesson[1063:12c03] wakeup. i: 1, thread: <NSThread: 0x711fcb0>{name = (null), num = 6}
2013-06-08 01:27:25.387 NSOperationLesson[1063:14d03] wakeup. i: 3, thread: <NSThread: 0x7126ea0>{name = (null), num = 5}
2013-06-08 01:27:25.387 NSOperationLesson[1063:12303] wakeup. i: 0, thread: <NSThread: 0x750fb70>{name = (null), num = 4}
2013-06-08 01:27:25.387 NSOperationLesson[1063:12e03] wakeup. i: 2, thread: <NSThread: 0x8a1f7a0>{name = (null), num = 3}
2013-06-08 01:27:27.391 NSOperationLesson[1063:14d03] wakeup. i: 5, thread: <NSThread: 0x7126ea0>{name = (null), num = 5}
2013-06-08 01:27:27.391 NSOperationLesson[1063:12303] wakeup. i: 6, thread: <NSThread: 0x750fb70>{name = (null), num = 4}
2013-06-08 01:27:27.391 NSOperationLesson[1063:12e03] wakeup. i: 7, thread: <NSThread: 0x8a1f7a0>{name = (null), num = 3}
2013-06-08 01:27:27.391 NSOperationLesson[1063:12c03] wakeup. i: 4, thread: <NSThread: 0x711fcb0>{name = (null), num = 6}
2013-06-08 01:27:29.395 NSOperationLesson[1063:12303] wakeup. i: 9, thread: <NSThread: 0x750fb70>{name = (null), num = 4}
2013-06-08 01:27:29.395 NSOperationLesson[1063:14d03] wakeup. i: 8, thread: <NSThread: 0x7126ea0>{name = (null), num = 5}
2013-06-08 01:27:29.396 NSOperationLesson[1063:12303] complete: __36-[ViewController operationWithQueue]_block_invoke_2


お、ログの出方が違う。queue 付きの方は、すぐに最後のログが出ているので、非同期で動いているようだ。
operation のみの方は、start でタスクが終了するまで待ってる気がする。


試しに queue の方で wait をかけてみると、こうなった。

2013-06-08 01:30:48.278 NSOperationLesson[1091:12c03] wakeup. i: 2, thread: <NSThread: 0x751de40>{name = (null), num = 6}
2013-06-08 01:30:48.278 NSOperationLesson[1091:14a0f] wakeup. i: 0, thread: <NSThread: 0x760ff20>{name = (null), num = 4}
2013-06-08 01:30:48.278 NSOperationLesson[1091:12303] wakeup. i: 1, thread: <NSThread: 0x7153380>{name = (null), num = 5}
2013-06-08 01:30:48.278 NSOperationLesson[1091:14d03] wakeup. i: 3, thread: <NSThread: 0x90231d0>{name = (null), num = 3}
2013-06-08 01:30:50.284 NSOperationLesson[1091:14a0f] wakeup. i: 4, thread: <NSThread: 0x760ff20>{name = (null), num = 4}
2013-06-08 01:30:50.284 NSOperationLesson[1091:14d03] wakeup. i: 7, thread: <NSThread: 0x90231d0>{name = (null), num = 3}
2013-06-08 01:30:50.284 NSOperationLesson[1091:12c03] wakeup. i: 5, thread: <NSThread: 0x751de40>{name = (null), num = 6}
2013-06-08 01:30:50.284 NSOperationLesson[1091:12303] wakeup. i: 6, thread: <NSThread: 0x7153380>{name = (null), num = 5}
2013-06-08 01:30:52.288 NSOperationLesson[1091:14d03] wakeup. i: 9, thread: <NSThread: 0x90231d0>{name = (null), num = 3}
2013-06-08 01:30:52.288 NSOperationLesson[1091:14a0f] wakeup. i: 8, thread: <NSThread: 0x760ff20>{name = (null), num = 4}
2013-06-08 01:30:52.289 NSOperationLesson[1091:12c03] complete: __36-[ViewController operationWithQueue]_block_invoke_2
2013-06-08 01:30:52.291 NSOperationLesson[1091:11303] done?

ふむ、やはり start だと wait がかかってるようだ。
start を非同期にする方法はないのだろうか? そう思ってリファレンスを見ると、
start はデフォルトで non-concurrent で、concurrent にしたけりゃ start 以下4つのメソッドを実装しろとのこと。
しかも OSX10.6 では無視されるとか書いてあるので、queue を使うのがよさそう。

感想

isConcurrent を replace すると非同期になったりするのだろうか…ちょっと気になる
そのうち試してみよう
→ 試してみたけど駄目でした。 フラグだけ立てればいいってものでもないらしい

iphoneアプリ開発日記その6

RSSリーダーを作ろう その2

フィードデータを取得する

さて、xml 解析が出来るようになったので、RSS フィードのデータを取得しませう

前回見たように、必要な情報はこんな感じ

  • channel か item か
  • title
  • link
  • description
  • pubDate

description 要らないような気がしますが、まあそのままいきましょう。


それで、これらの情報を格納する箱を定義します。
一番真っ当なやり方は、データクラスを作ってそれに突っ込むことでしょう。
しかし、今回は対して要素も多くないので、Dictionary に持たせてみましょう。

データ構造はこんな感じ

 (NSDictionary)
    + channnel (NSDictionary)
        + title
        + link
        + description
        + date
    + itemData (NSArray)
        + item (NSDictionary)
            + title
            + link
            + description
            + date
        + item...
             

で、これを処理するコードがこんなん

@interface RSSParser : NSObject <NSXMLParserDelegate>{
    NSXMLParser* parser;
    NSMutableDictionary* feedsDict;
    BOOL isChannel;
    BOOL isItem;
    BOOL isTitle;
    BOOL isLink;
    BOOL isDescription;
    BOOL isDate;
   
}
@implementation RSSParser

- (NSMutableDictionary*)parseRSS: (NSURL*)url{
 
    parser = [NSXMLParser.new initWithContentsOfURL:url];
    
    parser.delegate = self;
    
    [parser parse];
    
    return feedsDict;
    
}

- (void)parserDidStartDocument:(NSXMLParser *)parser{
    feedsDict = NSMutableDictionary.new;
    [feedsDict setObject:NSMutableDictionary.new forKey:FeedConstant.getChannelKey];
    [feedsDict setObject:NSMutableArray.new forKey:FeedConstant.getItemKey];
    
    isChannel = NO;
    isItem = NO;
    isTitle = NO;
    isLink = NO;
    isDescription = NO;
    isDate = NO;
    
}

- (void)parserDidEndDocument:(NSXMLParser *)parser{

    isChannel = NO;
    isItem = NO;
    isTitle = NO;
    isLink = NO;
    isDescription = NO;
    isDate = NO;

}

- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict{

    if ([elementName isEqualToString:FeedConstant.getChannelKey]){
        isChannel = YES;

    }else if ([elementName isEqualToString:FeedConstant.getItemKey]){
        isItem = YES;
        [[feedsDict objectForKey:FeedConstant.getItemKey] addObject:NSMutableDictionary.new];
        
    }else if ([elementName isEqualToString:FeedConstant.getTitleKey]){
        isTitle = YES;
    }else if ([elementName isEqualToString:FeedConstant.getLinkKey]){
        isLink = YES;
    }else if ([elementName isEqualToString:FeedConstant.getDescriptionKey]){
        isDescription = YES;
    }else if ([elementName isEqualToString:FeedConstant.getPubDateKey] || [elementName isEqualToString:FeedConstant.getDCDateKey]){
        isDate = YES;
    }
}
    


- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName{

    if ([elementName isEqualToString:FeedConstant.getChannelKey]){
        isChannel = NO;
        
    }else if ([elementName isEqualToString:FeedConstant.getItemKey]){
        isItem = NO;
        
    }else if ([elementName isEqualToString:FeedConstant.getTitleKey]){
        isTitle = NO;
    }else if ([elementName isEqualToString:FeedConstant.getLinkKey]){
        isLink = NO;
    }else if ([elementName isEqualToString:FeedConstant.getDescriptionKey]){
        isDescription = NO;
    }else if ([elementName isEqualToString:FeedConstant.getPubDateKey] || [elementName isEqualToString:FeedConstant.getDCDateKey]){
        isDate = NO;
    }
    

}

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string{
    NSString* trimmedString = [string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
    
    if (![trimmedString isEqualToString:@""]) {
        if (isChannel) {
            if (isTitle) {
                [[feedsDict objectForKey:FeedConstant.getChannelKey] setObject:string forKey:FeedConstant.getTitleKey];
            } else if (isLink) {
                [[feedsDict objectForKey:FeedConstant.getChannelKey] setObject:string forKey:FeedConstant.getLinkKey];
            } else if (isDescription) {
                [[feedsDict objectForKey:FeedConstant.getChannelKey] setObject:string forKey:FeedConstant.getDescriptionKey];
            }else if (isDate) {
                [[feedsDict objectForKey:FeedConstant.getChannelKey] setObject:string forKey:FeedConstant.getDateKey];
            }

        }else if (isItem) {
            if (isTitle) {
                [[[feedsDict objectForKey:FeedConstant.getItemKey] lastObject] setObject:string forKey:FeedConstant.getTitleKey];
            } else if (isLink) {
                [[[feedsDict objectForKey:FeedConstant.getItemKey] lastObject] setObject:string forKey:FeedConstant.getLinkKey];
            } else if (isDescription) {
                [[[feedsDict objectForKey:FeedConstant.getItemKey] lastObject] setObject:string forKey:FeedConstant.getDescriptionKey];
            } else if (isDate) {
                [[[feedsDict objectForKey:FeedConstant.getItemKey] lastObject] setObject:string forKey:FeedConstant.getDateKey];
            }
        }
        
    }
}

- (void)parser:(NSXMLParser *)parser parseErrorOccurred:(NSError *)parseError{
        NSLog(@"error: %@", parseError);
    
}

@end

これで必要なデータがとれました。

カテゴリを使ってみる

とりあえずフィード情報が使えるようになったのだが、Dictionary で下層までアクセスするのがめんどくさい。
なので、カテゴリーの練習がてらにNSDictionary を拡張することを考えた

カテゴリーとは、動的にメソッド追加できる機能のこと
Objective-C - Wikipedia

いろいろ制約はあるが、クラス定義を複数のファイルに分散できることになる。
また、wikiにもあるが基盤系(NS〜ってやつ)に簡単に機能追加できる。

これで NSDictionary にアクセサを追加してみた

こんな感じ。定数は適当に定義してください。

NSDictionary+FeedAccessor.h

#import <Foundation/Foundation.h>

@interface NSDictionary (FeedAccessor)

- (id)getChannelData;
- (id)getItemData;
- (NSString*)getTitle;
- (NSString*)getLink;
- (NSString*)getDescription;
- (NSString*)getDate;

@end

NSDictionary+FeedAccessor.m
#import "NSDictionary+FeedAccessor.h"
#import "FeedConstant.h"

@implementation NSDictionary (FeedAccessor)

- (id)getChannelData{
    return [self objectForKey:FeedConstant.getChannelKey];
}

- (id)getItemData{
    return [self objectForKey:FeedConstant.getItemKey];
}

- (NSString *)getTitle{
    return [self objectForKey:FeedConstant.getTitleKey];
}

- (NSString *)getLink{
    return [self objectForKey:FeedConstant.getLinkKey];
}

- (NSString *)getDescription{
    return [self objectForKey:FeedConstant.getDescriptionKey];
}

- (NSString *)getDate{
    return [self objectForKey:FeedConstant.getDateKey];
}

@end

使うときはこう

RSSParser *parser = RSSParser.new;
NSDictionary* feed = [parser parseRSS:[NSURL URLWithString:url]];
NSLog(@"%@", [feed getChannelData]getTitle]);

これでフィードの解析はできるようになりますた。
次はこれをパラレルで動かしてみよう