NSMutableArrayでつまずいた2つのこと


昨日NSMutableArrayを使っていて2カ所でつまずいたのでメモ。

高速列挙子でアクセス中の配列の要素を追加、削除しないこと!


for(NSString * str in ary){
	if([str isEqualToString:targetStr]){
		[ary removeObject:str];
	}
}

このようにFast Enumerationを使ってアクセスしているときに配列の要素を追加、削除すると

Collection was mutated while being enumerated

のエラーになる。


for(NSString * str in [ary reverseObjectEnumerator]){
	if([str isEqualToString:targetStr]){
		[ary removeObject:str];
	}
}

のようにして配列の最後から最初に向かって読み込むようにすると問題は解決する。
何番目の要素を操作するかを記録しておいて、forループの外で操作するようにしたりしてもいいかも。

参考
【コラム】ダイナミックObjective-C (105) Fast Enumeration(1) – 速い列挙子 | 開発・SE | マイナビニュース
Objective-Cと戦うブログ: 高速列挙でエラー「Collection was mutated while being enumerated.」が出た

NSUserDefaultsからmutableArrayを読み込むときはmutableCopyすること


NSMutableArray * ary = [[NSUserDefaults standardUserDefaults] objectForKey:aryKey];
[ary addObject:@"d"];

このようにすると以下のエラーになる。

'NSInternalInconsistencyException', reason: '-[__NSCFArray insertObject:atIndex:]: mutating method sent to immutable object'

「変更できないオブジェクトに、変更しようとするメソッドが送られたよ」と。
userdefaultsからNSMutableArrayを読み込むときは、immutableな配列が返ってくるので以下のようにmutableCopyしてやる必要があるらしい。


NSMutableArray * ary = [[[NSUserDefaults standardUserDefaults] objectForKey:aryKey] mutableCopy];
[ary addObject:@"d"];

マニュアルのobjectForKey:の項目を読むと、確かにimmutableになると書いてある。

Special Considerations
The returned object is immutable, even if the value you originally set was mutable.

が、参考になる記事が見つからなかったら結構悩んだに違いない。
先人に感謝。

参考
きみが思い出になる前に - NSUserDefaultsに保存したmutableなオブジェクトはmutableCopyを使って作り直す

Xcodeでカーソルのあるブロックをハイライト表示


こんな記事を見つけました。

Xcode4: 隠し機能?自分のいる場所をハイライトさせる | st.Prestage

こんな感じでカーソルがあるブロックをハイライト表示してくれるというもの。

続きを読む

Firefox mac版でページの拡大縮小

  • 2013年04月02日
  • mac

Safariではピンチアウト、ピンチインでWebページの拡大縮小ができます。
だがFirefoxではこれができない。

これが私がFirefoxをメインで使っていない唯一の原因でした。しかし調べてみると方法があるんですね、ちゃんと。
続きを読む

[PHP] curl_multiでhttpアクセスを並列化

  • 2013年03月12日
  • PHP

httpアクセスを並列化するには

今作っているのは自作アプリ用のwebサービス。
メインのphpのページから、同じドメイン内の20以上のphpファイルにアクセスする必要があります。
それぞれのphp内では外のサイトのデータを取得して解析するので、ものによっては実行完了までに数十秒かかります。

全てのphpへのアクセスを直列で行うのでは全体が完了するまでにかなりの時間を要するので、並列処理することにしました。

といってもphpは初心者なので、、、スレッドを生成するにはどうすればよいのか調べてみるとcrul_multiという関数群を使えば並列処理ができるということがわかりました。
PHP: cURL – Manual

curl_multi参考サイト

curl_multiを使った並列処理はあちらこちらで解説されているので、ほぼコピペで動かせそうなのもよいところです。
APIとの通信効率をよくする実装例(1) curl_multi – Yahoo! JAPAN Tech Blog
curl_multiでHTTP非同期リクエストを行うサンプル #PHP #HTTP – Qiita
phpでapiを使うなら必須なcurl_multi | エンジニア開発記
PHPでマルチスレッド(バックグラウンド処理)を実現する方法 – EC studio 技術ブログ

この辺を参考にして実際に動かしてみました。

原因不明のエラーが頻発

実行時間については期待した通り、全体の処理時間がボトルネックになるphpの処理時間+αくらいになりました。
しかし、計20ファイルに並列にアクセスするとそのうち1ファイルにエラーが発生することが頻繁に起こりました。
URLの配列を並べ替えて実行してみると、今度は異なるファイルにエラーが頻発します。
が、必ずエラーになるというわけでもない。。。

どこかに上限があるのかと、phpinfoで調べてみてmax_file_uploadsの値が20になっていたので、試しに40に変更してみましたが効果なし。

しばらく調べてみましたが、私には原因が特定できず。。。
レンタルサーバーなので、どこかで制限をかけているのでしょうか?
結局原因の特定を諦め、処理時間は伸びてしまうのですが10ファイルずつの並列処理を2回にわけて行い、最後に結果を結合させることで回避しました。

もしこのブログを読んで原因が分かる方がいらっしゃいましたら教えて下さい。

UIViewController + UITableView


■UITableViewControllerからUIViewController+UITableViewへ

今作っているアプリ。
当初UITableViewControllerを使って実装していたのだが、ナビゲーションバーとテーブルの間に広告を表示させるようにしたかった。
方法を模索するもどうやらUITableViewControllerで作ったUITableViewのframeは自由に変更できないような感じ。
そこで仕方なくUITableViewControllerを使わないようにして、UIViewControllerの派生クラスにUITableViewのインスタンスを持たせるように変更した。

こちらのサイトを参考にさせて頂いた。

UIViewControllerをつかって、UITableViewControllerを実装する

■変更時につまづいた箇所

基本的には上記のサイトのように実装を変更した。
が、UITableViewを使うのが初めてでまったく理解できていないのでかなり苦戦。
ベースの知識が違い過ぎるんだよなぁ。

・インターフェイス部にUITableViewDataSource,UITableViewDelegateの二つのプロトコルを採用していることを宣言する必要がある

<UITableViewDataSource,UITableViewDelegate>

・delegateとdatasourceの設定が必要

self.tableView.delegate = self;
self.tableView.dataSource = self;

・storyboard上のUITableViewを削除
これだけやってなんとなく動いていると勘違いしてしまったが、シミュレータ上ではstoryboardの中で設置していたUITableViewが見えていてコードで追加した方のUITableViewが使われていなかったので、storyboardの方のUITableViewを削除。
そういえばコードで追加した方のUITableViewはframeも設定していなければaddSubviewもしていなかったのだった・・・(情けない><)
コードで書くのならこれもやんなきゃね。

・dequeueReusableCellWithIdentifier:を使うように変更
storyboardの方のUITableViewを削除した関係で、cellForRowAtIndexPathの中でdequeueReusableCellWithIdentifier:forIndexPath:を使っていた箇所をdequeueReusableCellWithIdentifier:を使うように変更。
dequeueReusableCellWithIdentifier:forIndexPath:の方は、実行前にUITableViewCellを作成しておく必要があったのね。

Important: You must register a class or nib file using the registerNib:forCellReuseIdentifier: or registerClass:forCellReuseIdentifier: method before calling this method.

(※UITableView Class Referenceより)

こんな感じで無事動きました。

UIWebViewの上にインジケーターを表示させる


こちらのサイトを参考にさせて頂きました。
さくらのあたま: iphoneアプリ UIWebviewを呼び出しながら、UIActivityIndicatorViewを表示する方法

ほぼコピペしたのですが、インジケーターが見辛かったので、半透明のグレーの四角形の中にインジケータを表示させることにしました。

interface部分にUIViewを追加しました。

UIView * indicatorBackView;
UIActivityIndicatorView * indicator;


実装部分はこちら
(layerを使っているのでQuartzCore.hのインポートが必要)

#import 

// .....

- (void)viewDidLoad
        // ....
        // webViewの設置などをここまでに行っておいて
        // 以下インジケーターの設置部分のみ
	indicatorBackView = [[UIView alloc] initWithFrame:CGRectMake((self.view.bounds.size.width/2)-50, (self.view.bounds.size.height/2)-60, 100, 100)];
	indicatorBackView.backgroundColor = [UIColor grayColor];
	indicatorBackView.alpha = 0.5;
	[[indicatorBackView layer] setCornerRadius:5.0];
	[self.view addSubview:indicatorBackView];
	
	indicator = [[UIActivityIndicatorView alloc]initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
	indicator.frame = CGRectMake(indicatorBackView.bounds.size.width/2-20,indicatorBackView.bounds.size.height/2-20,40,40);
	[indicatorBackView addSubview:indicator];
}

//WEBの読み込みを開始したら
- (void)webViewDidStartLoad:(UIWebView*)aWebView {
	//インジケーターの表示
	indicatorBackView.hidden = NO;
	[indicator startAnimating];
	[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
}

//WEBの読み込み成功したら
- (void)webViewDidFinishLoad:(UIWebView*)aWebView {
	//インジケーターの非表示
	[indicator stopAnimating];
	indicatorBackView.hidden = YES;
	[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
}

//WEBの読み込みに失敗したら
- (void)webView:(UIWebView*)aWebView
didFailLoadWithError:(NSError*)error {
	//インジケーターの非表示
	[indicator stopAnimating];
	indicatorBackView.hidden = YES;
}


のようにしました。

実行すると、ページ読み込み時にはこのように表示されます。

iPhoneシミュレータへの画像の登録


画像を扱うアプリを作っているといろんな画像でテストをしたくなりますが、そのためにはシミュレータに画像を登録しなければなりません。

今まではシミュレータ上でsafariを開いて、適当な画像を見つけたら画像を長押しして登録していましたが、ときどきシミュレータ自体をリセットしたりするので、そうするとまた一から登録しなくちゃいけない。

また様々な形式の画像ファイルをテストする必要があると、自分で作成した画像をシミュレータに登録したくなります。

そこで画像を一気にシミュレータに登録する方法を調べました。
見つけたのはこちらの記事↓

[iPhone] iPhoneシミュレータに画像をまとめて転送 – Decremented Blog

簡単にまとめると、iPhone 6.0の場合

  • 以下のフォルダを削除(or リネーム)
    ユーザ/<ユーザ名>/ライブラリ/Application Support/iPhone Simulator/6.0/Media/PhotoData
  • 以下のフォルダの中に画像ファイルを放り込む
    ユーザ/<ユーザ名>/ライブラリ/Application Support/iPhone Simulator/6.0/Media/DCIM/100APPLE
  • シミュレータを起動してフォトライブラリにアクセス

最初やったときはフォトライブラリの中を見ても認識されていなかったのですが、一旦シミュレータを完全に終了させ、上記の手順をやり直したら見えるようになりました。
感謝!

言語設定による場合分け


iOSアプリの話。
日本語のときとそれ意外のとき(主に英語)で処理を分ける方法メモ。

NSString * language = [[NSLocale preferredLanguages] objectAtIndex:0];

    if([language compare:@"ja"] == NSOrderedSame) {
       // 日本語の場合の処理
    }else{
       // その他の言語の場合の処理
    }

参考にしたサイト
iPhoneの言語設定関連の情報取得についての解説 – 強火で進め

UIScrollViewでスクロールバーを常に表示


スクロール可能な画面を表示したときに、ユーザにスクロールできることがちゃんと伝わっているのか不安になりませんか?

標準のスクロールビューでできる工夫としては、以下のようにビューを表示したときにスクロールバーを一瞬表示させるとか、

[scrollView flashScrollIndicators];

または、以下のようにビューを少しスクロールさせた状態で表示しておいて、一番上までアニメーションで動かして気づかせるという方法などがあると思います。

[scrollView setContentOffset:CGPointMake(0.0f, 40.0f) animated:NO];
	[UIView animateWithDuration:1.0 animations:^{
		scrollView.contentOffset = CGPointMake(0.0f,0.0f);
	}];

でもスクロールバーを常に表示させておければこんな心配しなくてすむのにって思ったことないですか?
私はそう思って検索してみたのですが直接解決に導くページがヒットしなかったので、スクロールバーを常時表示するスクロールビューの派生クラスを即席で作ってみました。
もしかしたら適当なものでもいいのでソースコードが欲しいという方がいるかもしれないので公開します。

UIScrollViewの上にUIViewでスクロールバーを表示しただけの簡単なものです。
そして自分用に作ったので未完成な部分が多々あります。
例えば、insertSubviewはオーバーライドしていないので、スクロールバーの上に新たなビューが載ってしまうのでスクロールバーが見えなくなりますし、
delegateも定義していないのでスクロールやズームの開始終了の通知を受け取ることもできません。
私のアプリに不要だったので横スクロールにも非対応ですし、ズームにも非対応。
そしてまだデバッグも最小限しかしていないので致命的なバグがあるかも。。。

ScrollViewWithBar.h

#import <UIKit/UIKit.h>

@interface ScrollViewWithBar : UIScrollView

@property UIView * barView;
- (void)setBar;
@end

ScrollViewWithBar.m

#import "ScrollViewWithBar.h"
#import <QuartzCore/QuartzCore.h>

@implementation ScrollViewWithBar
@synthesize barView;

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
		[self setShowsVerticalScrollIndicator:NO];
		self.delegate = self;
		[self setBar];
    }
    return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];
    if (self) {
		[self setShowsVerticalScrollIndicator:NO];
		self.delegate = self;
		[self setBar];
    }
    return self;
}
- (id)init
{
    self = [super init];
    if (self) {
        // Initialization code
		[self setShowsVerticalScrollIndicator:NO];
		self.delegate = self;
		[self setBar];
    }
    return self;
}
- (void)setFrame:(CGRect)frame
{
	[super setFrame:frame];
	[self setBar];
}
- (void)setContentSize:(CGSize)contentSize
{
	[super setContentSize:contentSize];
	[self setBar];
}
- (void)addSubview:(UIView *)view
{
	[super addSubview:view];
	[self setBar];
}

- (void)setBar
{
	if(barView == nil){
		barView = [[UIView alloc] init];
		UIColor * barColor = [UIColor colorWithWhite:0.0 alpha:0.7];
		UIColor * backColor = [UIColor colorWithWhite:0.7 alpha:0.5];
		[barView setBackgroundColor:barColor];
		[[barView layer] setCornerRadius:3.0f];
		[[barView layer] setMasksToBounds:YES];
		[[barView layer] setBorderWidth:1.0f];
		[[barView layer] setBorderColor:backColor.CGColor];
		
		[self addSubview:barView];
		barColor = nil;
		backColor = nil;
	}else{
		[self bringSubviewToFront:barView];
	}
	float barWidth = 7.0;
	float barHeight = self.frame.size.height * self.frame.size.height / self.contentSize.height;
	
	barView.frame = CGRectMake(self.frame.size.width-barWidth-1,
								 0,
								 barWidth,
								 barHeight);
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
	CGRect barFrame = barView.frame;
	barFrame.origin.y = scrollView.contentOffset.y + scrollView.frame.size.height * scrollView.contentOffset.y / scrollView.contentSize.height;
	barView.frame = barFrame;
}
@end

アンドゥ処理の上限設定


今作っているアプリで、指でタッチして絵を描く処理があって、そこでアンドゥ処理ができるようにNSUndoManagerを導入しています。

やり方は割と簡単で、まずNSUndoManagerインスタンスを作って、

undoManager = [[NSUndoManager alloc] init];

アンドゥマネージャーにアウンドゥしたい処理を登録していくだけです。

例えば、このように登録しておくとスタックに貯めておいてくれて

[[undoManager prepareWithInvocationTarget:self] setColorAt:num Color:oldColor];

アンドゥを実行すると、

[undoManager undo];

登録したメソッドを以下のように実行してくれる仕組みです。

[self setColorAt:num Color:oldColor];

ひとまずうまく動いているようなので安心していたのですが、instrumentsを使ってメモリ使用量を見ていると、ずっと絵を描いていると徐々にメモリ使用量が増えていくことに気づきました。メモリリークしているのかと思ったのですが、メモリリークは検出されず。

それでcall treeを見ていくと、アンドゥマネージャーが肥大していることが分かりました。
そりゃあ全ての操作を記録していたらメモリを食いますよね。。。

そこでこちらのサイトを参考に上限を設定しました。
ツールはそろった: NSUndoManagerでスタック数の上限を設定する方法
コードはこのような感じです。

undoManager=[[NSUndoManager alloc]init];
[undoManager setLevelsOfUndo:30];

すると、無事メモリ使用量が安定しました。感謝!