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];

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

setNeedsDisplayInRect: での矩形の指定方法


安定動作のためのsetNeedsDisplayInRect:

ピンクのUIView Yの中のdrawRect:で線を描いたりしているのですが、ズームインしたときにはこのYを大きく拡大したりしています。
そのためsetNeedsDisplayでこのY全体を再描画していると、その瞬間にだけものすごい量のメモリを使ってアプリが落ちてしまったりしていました。
そこで、少なくとも再描画が必要な範囲(例の青の部分)というのを特定して、その範囲だけを再描画することにしました。

setNeedsDisplayInRect:の再描画範囲がおかしい?

しかし、ちゃんと再描画が必要な範囲を計算して、setNeedsDisplayInRect:に渡しているはずなのに、再描画される範囲がどう見てもおかしく正常動作しませんでした。
NSLogで渡している矩形を書き出したりして何度もチェックしたのですが、渡している矩形の範囲は数字上は正しいはずなのに、画面では再描画範囲が指定した範囲よりも右下にずれているような。。。?

値をいろいろと変えたり、あちこちにデバッグライトを入れてチェックしたりすること数時間、原因が分かりました。

原因は

setNeedsDisplayInRect:には、それを呼び出しているレシーバーを基準にした座標系のCGRectを渡す必要があったのでした。

上の例だと、私はUIView Xの座標系で計算したCGRectを渡しちゃっていたので、レシーバーであるUIView Yのoriginの分だけ右下にずれた範囲が再描画対象になっていたのでした。

UIView Yの中の青の部分だけを再描画させるためには、


CGRect r1 = CGRectMake(120,150,180,190); /* これじゃダメ */
CGRect r2 = CGRectMake(60,70,180,190); /* こちらが正解 */


r1だとダメで、r2をsetNeedsDisplayInRect:に渡してやる必要があります。
図形の変化に合わせてピンクのUIView Yのframeも変化していたので原因特定にすごく時間がかかりました。

実は、もちろんマニュアルにも書いてある。(なので誰も悩まないのでgoogleで検索しても解決できなかった・・・)

invalidRect
The rectangular region of the receiver to mark as invalid; it should be specified in the coordinate system of the receiver.

読んだはずなんだけどなぁ。
あんまりピンと来なくてスルーしてしまったのか?

教訓:マニュアルはちゃんと読もう!

storyboardをipad用に複製する方法


iPhone用に作ったstoryboardをiPad用に使いたいと思い、方法を調べました。

Stack Overflowより

見つかった情報はこちら。
Convert Storyboard from iPhone to iPad – Stack Overflow

1. iPhone用の storyboard を複製して、MainStoryboard_iPad.storyboard に名前を変える
2. テキストエディタで、このstoryboardを開く
3. targetRuntime=”iOS.CocoaTouch” という箇所を検索し、targetRuntime=”iOS.CocoaTouch.iPad” に置き換える
4. 保存してから、xcodeでこのstoryboardを開く。ただし、コンポーネントはバラバラ。
※ 原文は英語。適当に訳しました

追加で

  • テキストエディタで開いたときに、width=”320″ の箇所を width=”768″ に置換。height=”480″ の箇所を height=”1024″ に置換するといいかも。
  • テキストエディタは使わずに、コピーした新しいsotryboardをxcodeに追加した後、そのstoryboardを右クリック-> “open as” -> “Source Code” で書き換えればよい。

というようなコメントが書かれていました。

やってみた

最初Finderでstoryboardファイルを見つけるのに苦労しました。
よくよく探してみると、[プロジェクト名]->[en.lproj]配下にありました。ローカライズの作業なんかまったくやってないんですけど、これはstoryboardのファイルは自動でlpojに入れられる?
これをfinder上でコピーして名前をMainStoryboard_iPad.storyboardに変更して、xcodeにドラッグアンドドロップ。
次にxcode上でMainStoryboard_iPad.storyboardを右クリック-> “open as” -> “Source Code” で中身を書き換えます。

targetRuntime=”iOS.CocoaTouch.iPad” に書き換える、->”open as” -> “Interface Builder – iOS Storyboard”で開くと。。。
すべてのUIViewControllerが768×1024に変わるだけで、そこに載せているコンポーネントはそのままです。
320×480のサイズ指定した背景画像などは、widthをwidth=”768” に変更、heightをheight=”480″に変更すれば、ちょっとはマシになります。

キラキラしたアニメーションエフェクトを探して


アプリを使っているとアイテムを使用したときとか、目標を達成したときとかに、画面がキラキラ輝くエフェクトがあったりしますが、これってどうやって作っているのでしょう?
今作っているアプリでアイテムを使用するって場面があるんだけど、いまいちアイテムを使った感が得られなくて悩んでいます。
とりあえず「キラキラ」系の効果音をならしつつ、アイテム画像を2倍に拡大しながらうっすら消していく感じにしたんだけど、うーむイマイチ。

でもキラキラした画像を何枚も用意してアニメーションさせるなんて、考えただけでもウンザリ。
検索してもあんまりコレってページが見つからないんですよねぇ。

その中でかろうじて私が見つけることができたのがこちら。

有料ソフト Particle Designer

Particle Designer – iOS Game Particle Effects – Explosions Smoke Fire Rain Stars – iPhone iPad OSX Cocos2D

最初に見つけたのはコレ。
Particle(分子、粒子)ってことだから、粒子を好きなようにいじってキラキラできるんだろう。
値段も $7.99 ってことなので、高いものではないっていうかむしろ安いんだけど、極力有料ソフトは使いたくないんで買うことはないだろうなぁ。

Windows用のフリーソフト

エフェクトエディターの詳細情報 : Vector ソフトを探す!
次にこんなフリーソフトを発見。
どのようなソフトなのかまだチェックしていませんが、パラメータ指定で様々なエフェクトを作成して連番のpng形式のファイルを出力してくれるらしい。
こちらはフリーソフトなのでいずれさわってみようと思います。
どのようなものが作れるのか楽しみですが、最近windowsさわってなくて起動するのがちょっと面倒だったり。
mac版も作ってくれないかなぁ、なんて。

ちょっと元気があるときにでも一度自分でキラキラアニメ作ってみるかなぁ。
一度作れば使い回せそうだし。

UIColorを使いやすく


UIColor使いにくいなぁと思いながら我慢して使っていましたが、こんな工夫をされている方を見つけました。

UIColor を扱いやすくする « 水色のドア

Prefix.pch ファイルに

#define RGB(r, g, b) [UIColor colorWithRed:r/255.0 green:g/255.0 blue:b/255.0 alpha:1]
#define RGBA(r, g, b, a) [UIColor colorWithRed:r/255.0 green:g/255.0 blue:b/255.0 alpha:a]

を追加するという、ただそれだけで

UIColor * lineColor = RGB(153,153,255);

とか、

UIColor * foreColor = RGB(0x1e,0x90,0xff);

のように記述できるという。
便利そうなのでしばらく使ってみよう。

# Prefix.pchファイルの存在を今まで知らなかったというのは内緒。

==========追記(2013/01/03)==========
こんな記事を見つけました。
Objective-Cでよく使う便利マクロを10個集めてみた #iOS #iPhone #Objective-C #AdventCalendar – Qiita

この記事の中で同じようなUIColor用のマクロがありました。

#define RGB(r, g, b) [UIColor colorWithRed:(r)/255.0 green:(g)/255.0 blue:(b)/255.0 alpha:1]
#define RGBA(r, g, b, a)    [UIColor colorWithRed:(r)/255.0 green:(g)/255.0 blue:(b)/255.0 alpha:(a)]

私が最初に見つけたサイトにあったマクロとの違いは、引数にカッコがついていること。
こちらの記事の方も最初はカッコをつけていなかったようですが、コメントで指摘があったようです。

カッコをつけないと

RGB(200 + 55, 200 + 55, 200 + 55) 


のような使い方をするときに、意図した通りにならず

[UIColor colorWithRed:200+55/255.0 green:200+55/255.0 blue:200+55/255.0]


になってしまうんですね。

勉強になります。

Warning: Attempt to present 〜 on 〜 while a presentation is in progress!


警告が出るもよくわからず。。。

modalを

[self presentViewController:modalViewController animated:YES completion:nil];

で呼び出して、なんらかの処理をさせて、その後 delegate で戻ってきたところで

[self dismissViewControllerAnimated:YES completion:nil];

でmodalを閉じたりするんですが、そのときにタイトルに書いた


Warning: Attempt to present モーダル on 親のview while a presentation is in progress!

のような警告がでてきて悩んでました。

警告の意味がわからずgoogle先生にも聞いてみたところ、いつものstackoverflowさんが引っかかったのですが、それによるとmodalviewを開こうとしたら既にそのmodalviewは開かれている場合に出る警告ってことかな?
なぜこんな警告が出るんだろう。
そんなことをしているつもりはなくて、、、ちょっとよく意味が分からずしばらく放置していました。

原因が判明

この警告が出ていても一応正常に動いているように見えるし、他の箇所でmodalを表示しているところではこの警告は出てなかったりして、ほんと意味が分からず。
でも、別のバグ取りのためにステップ実行していたら理由が分かりました。

その理由というのが、viewWillAppear の中でmodalを表示させていたこと。

1. viewWillAppear が呼び出される
2. その中で presentViewController で modal を表示
3. delegateで戻ってきて dismissViewController で modal を閉じる
4. 再び viewWillAppear が呼び出される

modalの終了処理が終わる前にもう一度 modal を開こうとして警告が出ていたんだろうか。
もしかしたらそのままでもiOSが適切に対応してくれているのかもしれませんが、タイミングによっては大変なことになりはしないかとかなり心配だったので速攻で対処しました。

modalを閉じたときにも呼び出し元の viewWillAppear って呼ばれるんですね。
今まで知らなかった。
この警告で検索してもあまりヒットしないということは、常識なんだろうなぁ。
まだまだ力不足だなぁ。