CoreLocation - [3] MKMapView の初期表示設定

2010年10月13日水曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: CoreLocation - [2] マップに表示する

MKMapView はそのまま表示すると大西洋を中心とした世界地図が初期表示される。

今回はこの初期表示を現在地に変える。


MKCoordinateRegion


MKMapView で任意の場所を表示する場合、MKCoordinateRegionを使う。この MKCoordinateRegionはCの構造体で中心位置を表す CLLocationCoordinate2Dの値と、表示領域を示す MKCoordinateSpanの値を持つ。
typedef struct {
 CLLocationCoordinate2D center;
 MKCoordinateSpan span;
} MKCoordinateRegion;
中心位置は MKMapView を表示した時に中心にくる緯度経度を表す。MKCoordinateSpan は表示領域を表すために緯度経度方向の幅を表す値を持っている。
typedef struct {
    CLLocationDegrees latitudeDelta;
    CLLocationDegrees longitudeDelta;
} MKCoordinateSpan;
イメージはこんな感じ。
それぞれの構造体向けに生成用の関数が用意されているので利用時にはこれらを使う。
MKCoordinateSpan MKCoordinateSpanMake(
    CLLocationDegrees latitudeDelta, CLLocationDegrees longitudeDelta)

MKCoordinateRegion MKCoordinateRegionMake(
    CLLocationCoordinate2D centerCoordinate, MKCoordinateSpan span)
MKCoordinateRegion を用意できれば後は -[MKMapView setRegion:animated:] に渡して MKMapViewの表示を変えることができる。

それではサンプルで確認してみよう。


実装


指定した位置を中心に表示し、ピンを立てるメソッドを追加した。
- (void)setPinToCoordinate:(CLLocation*)location
{
 SimpleAnnotation* annotation = [[[SimpleAnnotation alloc] init] autorelease];
 annotation.location = self.locationManager.location; 
 [self.mapView addAnnotation:annotation];

 MKCoordinateSpan span = MKCoordinateSpanMake(0.5, 0.5);
 CLLocationCoordinate2D centerCoordinate = location.coordinate;
 MKCoordinateRegion coordinateRegion =
  MKCoordinateRegionMake(centerCoordinate, span);
 [self.mapView setRegion:coordinateRegion animated:YES]; 
}
これを起動直後と reloadボタンを押した時に呼び出す。


サンプル


実行してみよう。
一瞬大西洋が表示されるが、そこからアニメーションが始まり現在位置まで移動&ズームインする。


ソースコード


GitHubからどうぞ。
xcatsan's iOS-Sample-Code at 2010-10-13 - GitHub


参考情報


MKMapView Zoom and Reigon
MKCoordinateSpanの説明がわかりやすい。

Bugle Diary: [Objective-C][iPhone sdk][google maps]特定の住所を地図上に表示する
わかりやすくて参考になった。


その他


StackOverflow の記事で MKMapView の初期表示指定で GooglMapの Zoomlevelのような設定ができるブログを知った。
Set the Zoom Level of an MKMapView

GoogleMapだとこんな感じで書ける。
map.setCenter(new google.maps.LatLng(37.4419, -122.1419), 13);
最後の 13 が zoomlevel

この記事で紹介されている MKMapViewのカテゴリメソッドを使うと同等のことを行うのにこう書ける。
CLLocationCoordinate2D centerCoord = { 37.4419, -122.1419 };
    [mapView setCenterCoordinate:centerCoord zoomLevel:13 animated:NO];
}

zoomlevelも GoogleMapのそれと同じ縮尺になるよう調整してあるなどの凝りよう。ブログにはこのカテゴリを使って表示した地図とGoogleMapを同じ ZoomLevelを指定して比較したスクリーンショットまである。

これはなかなかいい。使えそう。

CoreLocation - [2] マップに表示する

2010年10月12日火曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: CoreLocation - [1] 現在地の緯度経度を取得する

今回は現在位置を地図上にピンを立てて表示してみる。

実装


MKMapView を使うにはまず MapKit.framework をプロジェクトへ追加する。

そして必要なヘッダをインポートする。
#import <MapKit/MapKit.h>

続いてコントローラにアウトレットとアクションを追加する。
@interface CoreLocationSampleViewController : UIViewController  {

 CLLocationManager* locationManager_;
 
 MKMapView* mapView_;
 
}

Interface Builder を開き MKMapView を配置した後、コントローラのアウトレットへ接続しておく。また "reload"ボタンを配置してやはりコントローラをターゲットとしておく。

次に MKAnnotationプロトコルを実装したクラスをひとつ用意する。
@interface SimpleAnnotation : NSObject <MKAnnotation>{

 CLLocation* location_;
}
@property (nonatomic, copy) CLLocation* location;

@end
@implementation SimpleAnnotation
@synthesize location = location_;

#pragma mark -
#pragma mark MKAnnotation
- (CLLocationCoordinate2D)coordinate
{
    return self.location.coordinate;
}

- (NSString*)title
{
    return @"Hello!";
}

@end

これをコントローラで "reload"ボタンが押された時に MKMapView へ追加する。
#pragma mark -
#pragma mark Event
- (IBAction)reload:(id)sender
{
 SimpleAnnotation* annotation = [[[SimpleAnnotation alloc] init] autorelease];
 annotation.location = self.locationManager.location; 
 [self.mapView addAnnotation:annotation];
}


実行


さて実行してみよう。
実行直後は大西洋を中心とした世界地図が表示される。

reloadボタンを押した後、日本を探すと...あった。

拡大。

ピンをクリックすると -[MKAnnotation title] の文字列が噴出しで表示される。


ソースコード


GitHubからどうぞ。
CoreLocationSample at 2010-10-12 from xcatsan's iOS-Sample-Code - GitHub


参考情報


【コラム】実践! iPhoneアプリ開発 (16) ロギングアプリの作り方 (2) - Map Kitで地図を表示する | エンタープライズ | マイコミジャーナル

美容師向けアプリ hair Concierge(ヘア・コンシェルジェ)の技術解説

2010年10月11日月曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

私が開発に参加した初めての iPhoneアプリがリリースされました。美容師向けに特化した顧客管理アプリです。


知人の会社から無料アプリとして公開されています。

写真などのサンプルデータが初めから用意されているのでダウンロードすればすぐに試すことができます。美容師では無い人も無料なので良かったら試してみてください。アニメーションを使ったちょっとしたギミックが盛り込まれたアプリになっています。効果音も付いているので試すときは音も聴いてみてください。


アプリの紹介は公式サイト他に任せるとして、このブログではアプリで使った技術的な内容について紹介します。


体制と期間


  • 開発はプログラマ1人、デザイナー1人、ディレクター1人の3人体制
  • 私はプログラマとして参加
  • 期間は実質3ヶ月くらい
  • 開発の為に MacBookPro を新調し開発

以下、技術トピック。



顧客リスト



スライドアニメーション
顧客情報をタップするとボタンと詳細情報が載ったトレイがスライドして現れる。UITableView のセルの高さを変えるだけでいい具合のアニメーションができる。

検索機能
UISearchDisplayController を利用している。以下,関連記事。
Cocoaの日々: UISearchDisplayController 調査
Cocoaの日々: UISearchDisplayController と NSFetchedResultContoller を組み合わせる (3) 考察
Cocoaの日々: UISearchDisplayController で用意される UITableView を扱う上での注意点
Cocoaの日々: UISearchDisplayBar を初期状態では隠しておく

未登録時
顧客が未登録の場合、専用のカスタムセルを用意して表示している(通常のセルと同様フリックして上下に動かせる)。


顧客情報


カスタムセル
UITableView でカスタムセルを用意し外観をすべて独自のデザインにしている。


他の画面も同じカスタムセルを使ったデザインとなっている。




ヘッダの開閉


UITableView上の開閉する画像は UITableView.tableViewHeaderを使っている。UITableView.tableViewHeader は表示後に高さを変えても見た目が変化せず、アニメーションしない。それをうまくアニメーションさせて開閉しているように見せるためにダミーセルを使う。
Cocoaの日々: UITableView のヘッダの高さを変える(その2:アニメーション)


カルテ詳細


カスタムセルに、カスタムセクションヘッダを使用。


(旧) Cocoa Touch の日々: NSFetchedResultsController でグルーピング(Section分け)


カルテ情報


2ステップアニメーション
ヘッダをタップすると2ステップで開閉するアニメーションが始まる。



横並びサムネイル画像の循環スクロール

横並びサムネイル画像は以前紹介した実装が入っている。この横並び画像は指で循環スクロールし、一定時間操作が無いと自動的に横スクロールが始まる。
Cocoaの日々: 画像を横に並べたスクロールビューアの作成 [1] アイディア



カルテ設定


上段の選択状態によって「詳細設定」のドロアーが上下に開閉する。



設定 - 使用状況とエクスポート


CSV作成とZIP圧縮
CSV生成後に ZIP圧縮してメール送付する。


Cocoaの日々: Objective-Zip を使って ZIP圧縮する
Cocoaの日々: 拡張子から MIME Type を取得する
メールへファイルを添付する際に指定する MIME type を取得するのに利用。

Cocoaの日々: CoreData - 大量データを扱う場合のメモリ利用量を減らす
仕様として掲げた 6,000件のカルテデータの CSV書き出し時にメモリ不足でクラッシュした問題の対応方法。

Cocoaの日々: ファイル書き出し
Cocoaの日々: CSVの改行コードなど


オリジナルダイアログ



Cocoaの日々: 下からせり上がってくる非モーダルなカスタムダイアログを作る (2)二段構え


画像ビューア





Cocoaの日々: UIScrollView - 拡大縮小
Cocoaの日々: UIScrollView - フリックで画像(ページ)をめくる
Cocoaの日々: ステータスバー、ナビゲーションバー、ツールバーを半透明にする
Cocoaの日々: 簡易スライドビューア [1]基本動作


画面ロック


パスコード入力で3回失敗すると表示される。「ギリギリ」と音を立てながら中央のシャッターが閉まる。UIImageViewのアニメーションで実現。



画像系


Cocoaの日々: iOS4からデバイス毎・解像度毎に用意した画像を自動選択する仕組みが導入された


効果音系


Cocoaの日々: 効果音を鳴らす

Cocoaの日々: 効果音販売サイトの紹介と iTunesを使ったMP3からAIFFへの変換方法

Cocoaの日々: オーディオフォーマット変換 afconvert


UITableView


Cocoaの日々: UITableView - 編集モードで左側のアイコンを消す

Cocoaの日々: UITableView の背景に画像を表示する


日付関連


Cocoaの日々: NSCalendar - 2つの
日付間の日数を取得する



NSFetchedResultController


アプリのリスト画面はすべて NSFetchedResultController を利用している。

Cocoaの日々: NSFetchedResultsController のおさらい

Cocoaの日々: UISearchDisplayController と NSFetchedResultContoller を組み合わせる

Cocoaの日々: NSFetchedResultsControllerDelegate を使う

Cocoaの日々: -[NSFetchedResultsController performFetch:] でクラッシュ


データ


Core Data を使っている。マスタは XML で用意しバンドルしている。

Core Data 集計系
Cocoaの日々: CoreData - 最大値をもつ NSManagedObject を取得するコード見本

Cocoaの日々: CoreData - 集計関数

Core Data モデリング
Cocoaの日々: Core Data - Unidirectional Relationships(単方向関連)について

Core Data その他
Cocoaの日々: -[NSFetchedResultsController performFetch:] でクラッシュ

その他、紹介してきたすべてがノウハウとして使われている。
Cocoaの日々: Core Data
(旧) Cocoaの日々: coredata
(旧) Cocoa Touch の日々: CoreData

マスタ
XMLでバンドルしている。Category.plist のイメージ。



アニメーション


遅延実行の Blocks化は2ステップアニメーションなどで役立った。
Cocoaの日々: 遅延実行を Blocksで記述する


その他


Cocoaの日々: Xcode - Build And Analyze

Cocoaの日々: Xcode - Build And Analyze 〜 問題の例

Cocoaの日々: Subversion で @ を含むファイルを扱う

Cocoaの日々: NSUserDefaults - アプリケーション出荷時設定

Cocoaの日々: アプリのバージョン番号を取得する(メモ)


Xcodeプロジェクト構成


おまけで公開。




お世話になった本




UIKitでやりたいこと、困ったことがあればこの本が役に立つ。




最初の一歩を踏み出すのに役立った。




表題・表紙と裏腹に意外と実用的。




広く浅くだが実用的でなかなか役立つ。

UITableViewを並べて横スクロールしてみる

2010年10月10日日曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

3つの UITableViewを UIScrollView に載せてフリックで横スクロールするユーザインターフェイスを試してみた。

サンプル



横にフリックすると

スクロールして 隣の UITableView へ移る。


実装


現在作成中の簡易スライドショーの実装をベースにして基本的には UIImageView を UITableView に切り替えただけ。
Cocoaの日々: 簡易スライドビューア [1]基本動作
ただ UITableViewの数は3つ固定としてある。また UITableView内のデータも固定の値。

イメージはこんな感じ。


詳細はソースコードを参考のこと。



ソースコード


GitHubからどうぞ。
HorizontalScrollableUITableView at 2010-10-10 from xcatsan's iOS-Sample-Code - GitHub


考察


実機で確認すると意外と使いづらいことがわかった。親指を使ったフリックの方向は真横ではなく実際は斜めになるため、先にUITableViewの縦スクロールが発生してしまうことがしばしば起こる。このままだと横スクロールをしたいのに縦スクロールしてしまい非常にもどかしい思いをする(イライラする)。このあたりは、横のフリック量が一定以上発生する場合は、UITableView標準の縦スクロールを禁止もしくは制動するような調整が必要だと思った。

また UITableView を横に並べるアイディアはみんな考えているようで、ネットで探すといくつか見つかる。ただいろいろなところで指摘されているのは、UITableView の横フリックはレコード削除操作に割り当てられているので、横スクロールしてしまうと標準から逸脱してしまうこと。この点はユーザにとって使いやすいといえるかどうか、心にひっかかる。

そういうわけで、この UI がアプリで使えるかどうかは今のところ微妙。縦スクロール抑制は時間があれば試してみたい。


参考情報


Cocoaの日々: UIScrollView - ページスクロールで空白を挟む
UITableViewの間に挟んである黒い空白を入れる方法。

x
x

x

CoreLocation - [1] 現在地の緯度経度を取得する

2010年10月9日土曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

CoreLocation を使って現在地の緯度経度を取得する。簡単なサンプルを作ってシミュレータ、iPhone3GS、iPadで動作確認をやってみた。

CoreLocation


CoreLocationManager を使う。使い方はリファレンスに書いてある。
CLLocationManager Class Reference より抜粋

To configure and use a CLLocationManager object to deliver events:
  1. Always check to see whether the desired services are available before starting any services and abandon the operation if they are not.
  2. Create an instance of the CLLocationManager class.
  3. Assign a custom object to the delegate property. This object must conform to theCLLocationManagerDelegate protocol.
  4. Configure any additional properties relevant to the desired service.
  5. Call the appropriate start method to begin the delivery of events.


サンプル


起動するとデバッガコンソールへ CLLocationの情報を書き出す簡単なサンプルを作った。
[3389:307] Start updating location.
[3389:307] ----------------------------------------------------
[3389:307] latitude,logitude : 35.433066, 139.720322
[3389:307] altitude          : 0.000000
[3389:307] cource            : -1.000000
[3389:307] horizontalAccuracy: 250.000000
[3389:307] verticalAccuracy  : -1.000000
[3389:307] speed             : -1.000000
[3389:307] timestamp         : 2010-10-20 06:31:09 +0900
画面表示は何もなし。



実装


準備

(1) Frameworks に CoreLocation.frameworkを追加する

(2) ヘッダファイル読み込み
#import <CoreLocation/CoreLocation.h>

コード


- (void)viewDidLoad {
    [super viewDidLoad];

 if ([CLLocationManager locationServicesEnabled]) {
  self.locationManager = [[CLLocationManager alloc] init];
  self.locationManager.delegate = self;
  [self.locationManager startUpdatingLocation];
  NSLog(@"Start updating location.");
  
 } else {
  NSLog(@"The location services is disabled.");
 }
}

- (void)logLocation:(CLLocation*)location
{
 CLLocationCoordinate2D coordinate = location.coordinate;
 NSLog(@"----------------------------------------------------");
 NSLog(@"latitude,logitude : %f, %f", coordinate.latitude, coordinate.longitude);
 NSLog(@"altitude          : %f", location.altitude);
 NSLog(@"cource            : %f", location.course);
 NSLog(@"horizontalAccuracy: %f", location.horizontalAccuracy);
 NSLog(@"verticalAccuracy  : %f", location.verticalAccuracy);
 NSLog(@"speed             : %f", location.speed);
 NSLog(@"timestamp         : %@", location.timestamp);
}

- (void)locationManager:(CLLocationManager *)manager
 didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation {

 [self logLocation:newLocation];
}

- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error{
 NSLog(@"Error: %@", error);
}



結果


シミュレータ (WiFi)

初回に確認ダイアログが表示される。

1回目
Start updating location.
----------------------------------------------------
latitude,logitude : 35.534066, 139.720422
altitude          : 0.000000
cource            : -1.000000
horizontalAccuracy: 150.000000
verticalAccuracy  : -1.000000
speed             : -1.000000
timestamp         : 2010-10-19 21:44:07 GMT
----------------------------------------------------
latitude,logitude : 35.533892, 139.720450
altitude          : 0.000000
cource            : -1.000000
horizontalAccuracy: 185.000000
verticalAccuracy  : -1.000000
speed             : 0.000000
timestamp         : 2010-10-19 21:44:38 GMT
----------------------------------------------------
latitude,logitude : 35.533892, 139.720450
altitude          : 0.000000
cource            : -1.000000
horizontalAccuracy: 185.000000
verticalAccuracy  : -1.000000
speed             : 0.000000
timestamp         : 2010-10-19 21:44:53 GMT

2回目
Start updating location.
----------------------------------------------------
latitude,logitude : 35.533892, 139.720450
altitude          : 0.000000
cource            : -1.000000
horizontalAccuracy: 185.000000
verticalAccuracy  : -1.000000
speed             : 0.000000
timestamp         : 2010-10-19 21:45:13 GMT
----------------------------------------------------
latitude,logitude : 35.533892, 139.720450
altitude          : 0.000000
cource            : -1.000000
horizontalAccuracy: 185.000000
verticalAccuracy  : -1.000000
speed             : 0.000000
timestamp         : 2010-10-19 21:45:23 GMT

実機 - iPhone 3GS (iOS4.0) ※屋内

初回にダイアログが表示される。

また実行中は位置情報使用中のインジゲータが表示される。

1回目 6:47起動
Start updating location.
----------------------------------------------------
latitude,logitude : 35.530754, 139.718766
altitude          : 0.000000
cource            : -1.000000
horizontalAccuracy: 620.000000
verticalAccuracy  : -1.000000
speed             : -1.000000
timestamp         : 2010-10-20 06:46:50 +0900
----------------------------------------------------
latitude,logitude : 35.530754, 139.718766
altitude          : 0.000000
cource            : -1.000000
horizontalAccuracy: 620.000000
verticalAccuracy  : -1.000000
speed             : -1.000000
timestamp         : 2010-10-20 06:46:50 +0900
----------------------------------------------------
latitude,logitude : 35.533505, 139.720757
altitude          : 0.000000
cource            : -1.000000
horizontalAccuracy: 80.000000
verticalAccuracy  : -1.000000
speed             : -1.000000
timestamp         : 2010-10-20 06:46:52 +0900
  :

2回目 6:47起動
Start updating location.
----------------------------------------------------
latitude,logitude : 35.533505, 139.720757
altitude          : 0.000000
cource            : -1.000000
horizontalAccuracy: 80.000000
verticalAccuracy  : -1.000000
speed             : -1.000000
timestamp         : 2010-10-20 06:47:47 +0900
  :

3回目 6:53起動(5分後)
Start updating location.
----------------------------------------------------
latitude,logitude : 35.533505, 139.720757
altitude          : 0.000000
cource            : -1.000000
horizontalAccuracy: 80.000000
verticalAccuracy  : -1.000000
speed             : -1.000000
timestamp         : 2010-10-20 06:48:22 +0900
----------------------------------------------------
latitude,logitude : 35.533505, 139.720757
altitude          : 0.000000
cource            : -1.000000
horizontalAccuracy: 80.000000
verticalAccuracy  : -1.000000
speed             : -1.000000
timestamp         : 2010-10-20 06:54:22 +0900
----------------------------------------------------
latitude,logitude : 35.533505, 139.720757
altitude          : 0.000000
cource            : -1.000000
horizontalAccuracy: 80.000000
verticalAccuracy  : -1.000000
speed             : -1.000000
timestamp         : 2010-10-20 06:55:25 +0900
----------------------------------------------------
latitude,logitude : 35.533658, 139.720645
altitude          : 0.000000
cource            : -1.000000
horizontalAccuracy: 250.000000
verticalAccuracy  : -1.000000
speed             : -1.000000
timestamp         : 2010-10-20 06:56:27 +0900


実機 - iPad (iOS 3.2/WiFi)

1回目(6:52起動)
Start updating location.
----------------------------------------------------
latitude,logitude : 35.533658, 139.720645
altitude          : 0.000000
cource            : -1.000000
horizontalAccuracy: 250.000000
verticalAccuracy  : -1.000000
speed             : -1.000000
timestamp         : 2010-10-20 06:52:15 +0900
----------------------------------------------------
latitude,logitude : 35.533658, 139.720645
altitude          : 0.000000
cource            : -1.000000
horizontalAccuracy: 250.000000
verticalAccuracy  : -1.000000
speed             : -1.000000
timestamp         : 2010-10-20 06:53:17 +0900

2回目(6:53起動)
Start updating location.
----------------------------------------------------
latitude,logitude : 35.533658, 139.720645
altitude          : 0.000000
cource            : -1.000000
horizontalAccuracy: 250.000000
verticalAccuracy  : -1.000000
speed             : -1.000000
timestamp         : 2010-10-20 06:53:52 +0900


まとめ

  • 実機(iPhone3GS)では1分毎に CLLocationManagerDelegate のメソッドが呼び出される(位置情報が更新される)
  • 実機(iPhone3GS)では位置情報更新の度に精度 が上がっていった(CLLocation.horizontalAccuracyが低くなる)。ただし悪くなるケースも見られた。
  • 5分程度してから位置情報へアクセスすると前回キャッシュされた位置情報が最初に返された(CLLocation.timestampで確認可能)。
  • シミュレータでも WiFi経由で現在位置の取得が可能。確認ダイアログも表示される(昔はそうではなかったらしい)
  • locationServicesEnabledプロパティはiOS4.0から Deprecated。同名のクラスメソッドを使う。
  • 検証環境での精度は
    シミュレータ [150〜185m]
    iPhone3GS [80〜250m]
    iPad-Wifi [250m] ※固定?
  • CLLocation.verticalAccuracyはiOSの場合常に-1(リファレンスに書いてある)。

ソースコード


GitHub からどうぞ。
xcatsan's iOS-Sample-Code at 2010-10-09 - GitHub


参考情報


CLLocationManager Class Reference

リファレンス


LocateMe

iOS Reference Library のサンプル


【コラム】実践! iPhoneアプリ開発 (15) ロギングアプリの作り方 (1) - Core Locationで現在地を取得する | エンタープライズ | マイコミジャーナル
【コラム】実践! iPhoneアプリ開発 (16) ロギングアプリの作り方 (2) - Map Kitで地図を表示する | エンタープライズ | マイコミジャーナル
【コラム】実践! iPhoneアプリ開発 (17) ロギングアプリの作り方 (3) - アノテーションビューをカスタマイズする | エンタープライズ | マイコミジャーナル

木下氏の連載。わかりやすくとても参考になった。


GPSを利用する方法 - プログラミングノート

ソースコードを参考に。


Core Locationのaccuracyについて - The iPhone Development Playground

精度の話。タイムスタンプチェックや精度を上げる為には数回位置更新を待つ、など。昨年の記事なので iOS 3ベース。

- - - - -
今更ながら初めて CoreLocation を試してみた。なんか楽しい-。
次回はマップにピンを立てる。

viewDidUnload は呼ばれない(メモリ不足時だけ呼ばれる)

2010年10月8日金曜日 | Published in | 2 コメント

このエントリーをはてなブックマークに追加

UIViewControllerの画面を閉じる時に通常 viewDidUnloadが呼び出されることは無い。このメソッドが呼び出されるのはメモリ不足の時のみ。名前が viewDidLoad と対になっているが、動作は対になっていない。


UIViewController の各種メソッド呼び出しタイミング


通常の動作
viewDidLoad
 |
 |閉じる
 ↓
dealloc

メモリ不足発生時の動作
viewDidLoad
 |
 |メモリ不足発生
 ↓
didReceiveMemoryWarning
 |
viewDidUnload
 |
 :
 ↓
viewDidLoad
 |
 |閉じる
 ↓
dealloc
てっきり通常 viewDidUnload が呼び出されると勘違いしていたがそうではない。
となると、メモリの解放は viewDidUnload だけでは駄目で dealloc にも実装しないとメモリリークが発生することになる。サンプルを作って確認してみた。


サンプル


RootとSub、SubSubの3つの UIViewController を用意して、SubViewController上の UIImageViewの retainCount を確認する。


RootViewController 上の childImageView プロパティは、SubViewControllerを閉じて deallocが呼ばれた後に retainCountがどうなっているかを確認する為に assign で参照している(assignだと retainCountは増えない)。


RootViewControllerのコード
@interface RootViewController : UIViewController {
 UIImageView* childImageView;
}

@property (nonatomic, assign) UIImageView* childImageView;

- (IBAction)next:(id)sender;

@end
- (void)printRetainCount
{
 NSLog(@"[Root] retainCount=%d", [self.childImageView retainCount]-1);
}

- (void)viewDidAppear:(BOOL)animated {
 
 if (self.childImageView) {
  [self performSelector:@selector(printRetainCount)
       withObject:nil
       afterDelay:1.0];
 }
}
- (IBAction)next:(id)sender
{
 SubViewController* vc = [[SubViewController alloc]
            initWithNibName:@"SubViewController" bundle:nil];
 vc.rootViewController = self;
 [self.navigationController pushViewController:vc animated:YES];
 [vc release];
}
SubViewControllerを閉じて RootViewController へ戻ってきた時に viewDidAppear: で retainCountを表示している。なお SubViewControllerの dealloc の後で確実に確認するために1秒間の遅延をもたせている。

次に SubViewController。
@class RootViewController;
@interface SubViewController : UIViewController {
 
 UIImageView* imageView;
 RootViewController* rootViewController;

}

@property (nonatomic, retain) IBOutlet UIImageView* imageView;
@property (nonatomic, retain) RootViewController* rootViewController;

-(IBAction)next:(id)sender;

@end

@implementation SubViewController
@synthesize imageView;
@synthesize rootViewController;

- (void)viewDidLoad {
    [super viewDidLoad];

 // for debug
 [self.imageView retain];

 NSLog(@"[Sub ] viewDidLoad|retainCount=%d", [self.imageView retainCount]-1);
 
 rootViewController.childImageView = self.imageView;
}

- (void)didReceiveMemoryWarning {
    NSLog(@"[Sub ] didReceiveMemoryWarning|retainCount=%d", [self.imageView retainCount]-1);
    [super didReceiveMemoryWarning];    
}

- (void)viewDidUnload {
    [super viewDidUnload];
 NSLog(@"[Sub ] viewDidUnload|retainCount=%d", [self.imageView retainCount]-1);
 self.imageView = nil;
}

- (void)dealloc {
    NSLog(@"[Sub ] dealloc|retainCount=%d", [self.imageView retainCount]-1);
    [super dealloc];
}

- (IBAction)next:(id)sender
{
 SubSubViewController* vc = [[SubSubViewController alloc]
           initWithNibName:@"SubSubViewController" bundle:nil];
 [self.navigationController pushViewController:vc animated:YES];
 [vc release];
}
@end
retainCount==0 のケースはメモリ解放後のオブジェクトになる為、実行時には retainCountメッセージを送るとアプリがクラッシュする。そこで -[SubViewController viewDidLoad]内で retain して+1水増ししておき、表示する箇所で−1して正しい数値に直している。

実行してみよう。まず通常のケース。
(1)pushViewController:SubViewController
(2)popViewController

実行結果
[Sub ] viewDidLoad|retainCount=2
[Sub ] dealloc|retainCount=2
[Root] retainCount=1
retainCount=2の内訳は [1]UIImageViewの親ビューによるretain [2]@property(retain)宣言とIBOutlet接続によるretainによる。また、viewDidUnload が呼ばれていないことがわかる。

一方、RootViewController へ戻った時に retainCountが1残っている。SubViewController は dealloc が呼ばれているので破棄されたことになるが、IBOutlet(retain)で接続した UIImageViewはメモリに残ったまま、つまりメモリリークが起きているのがわかる。これは dealloc 内で IBOutlet(retain)接続した UIImageViewの解放を行っていないため。viewDidUnloadで解放(=nil)しているが、このメソッドは通常は呼ばれない。

つまり IBOutlet(retain)接続したオブジェクトは deallocで解放しない場合メモリリークを引き起こす。

dealloc内に解放コードを追加してみる。
- (void)dealloc {
 NSLog(@"[Sub ] dealloc|retainCount=%d", [self.imageView retainCount]-1);
 
 self.imageView = nil;
    [super dealloc];
}

実行結果
[Sub ] viewDidLoad|retainCount=2
[Sub ] dealloc|retainCount=2
[Root] retainCount=0
retainCountは0となり、メモリリークが起きていない。


最後にメモリ不足をシミュレートしたケース (dealloc内解放コードあり)。

(1)pushViewController:SubViewController
(2)pushViewController:SubSubViewController
(3)メモリ不足をシミュレート
(4)popViewController
(5)popViewController

[Sub ] viewDidLoad|retainCount=2
Received simulated memory warning.
[Sub ] didReceiveMemoryWarning|retainCount=3
[Sub ] viewDidUnload|retainCount=1
[Sub ] viewDidLoad|retainCount=2
[Sub ] dealloc|retainCount=2
[Root] retainCount=0

メモリ不足発生直後に viewDidUnload が呼ばれているのがわかる。


奇妙なのが didReceiveMemoryWarningが呼ばれた時点で retainCountが3に増えている。これはなんだろう?

試しに Root => Sub => SubSub のケースをメモリ不足なしで実行してみた。
[Sub ] viewDidLoad|retainCount=2
[Sub ] dealloc|retainCount=3
[Root] retainCount=0
すると SubViewController の dealloc 時点では retainCountが3になっている。Sub => SubSub へ遷移することによって何故か retainCountが増加している。しかも Rootに戻った時はきちんと0にもどっている。

retainCount がどの時点で増えたか確認するため、SubSubController が表示された時点での retainCountを確認するコードを追加する。
@interface SubSubViewController : UIViewController {
 UIImageView* parentImageView;
}
@property (nonatomic, assign) UIImageView* parentImageView;


@end
- (void)viewDidLoad {
    [super viewDidLoad];
 NSLog(@"[SubSub] viewDidLoad|retainCount=%d", [self.parentImageView retainCount]-1);
  
}
- (void)viewWillAppear:(BOOL)animated {
 NSLog(@"[SubSub] viewWillAppear|retainCount=%d", [self.parentImageView retainCount]-1);
 
}
- (void)viewDidAppear:(BOOL)animated {
 NSLog(@"[SubSub] viewDidAppear|retainCount=%d", [self.parentImageView retainCount]-1);
 
}

SubViewControllerへ以下を追加
- (void)viewWillDisappear:(BOOL)animated
{
 NSLog(@"[Sub ] viewWillDisappear|retainCount=%d", [self.imageView retainCount]-1);
}
- (void)viewDidDisappear:(BOOL)animated
{
 NSLog(@"[Sub ] viewDidDisappear|retainCount=%d", [self.imageView retainCount]-1);
}
- (IBAction)next:(id)sender
{
          :
 [self.navigationController pushViewController:vc animated:YES];
 vc.parentImageView = self.imageView;
          :
}

実行結果
[Sub ] viewDidLoad|retainCount=2
[SubSub] viewDidLoad|retainCount=3
[Sub ] viewWillDisappear|retainCount=3
[SubSub] viewWillAppear|retainCount=3
[Sub ] viewDidDisappear|retainCount=3
[SubSub] viewDidAppear|retainCount=3
[Sub ] viewWillDisappear|retainCount=3
[Sub ] viewDidDisappear|retainCount=3
[Sub ] dealloc|retainCount=3
[Root] retainCount=0
SubSubViewControllerを pushした時点で retainCountが1増加している。理由はわからず。うーむ?
(わかる方、どうぞコメントに書き込んで教えて下さい)

メモリ不足シミュレート時の動作も記録として残しておく。
[Sub ] viewDidLoad|retainCount=2
[SubSub] viewDidLoad|retainCount=3
[Sub ] viewWillDisappear|retainCount=3
[SubSub] viewWillAppear|retainCount=3
[Sub ] viewDidDisappear|retainCount=3
[SubSub] viewDidAppear|retainCount=3
Received simulated memory warning.
[Sub ] didReceiveMemoryWarning|retainCount=3
[Sub ] viewDidUnload|retainCount=1
[Sub ] viewDidLoad|retainCount=2
[Sub ] viewWillDisappear|retainCount=2
[Sub ] viewDidDisappear|retainCount=2
[Sub ] dealloc|retainCount=2
[Root] retainCount=0


ソースコード


GitHubからどうぞ。
ViewControllerMemorySample at 2010-10-08b from xcatsan's iOS-Sample-Code - GitHub


まとめ


viewDidLoad で確保したメモリは、viewDidUnload および dealloc 両方で解放する

以前書いた記事では viewDidLoadで確保したメモリの deallocでの解放には触れていなかった。これは後日訂正する。
Cocoaの日々: UIViewController でのメモリ管理見本
解放コードのサンプルは(後日)そちらを参照されたい。


参考情報


viewDidUnload と dealloc でのメモリ解放の話題は調べてみると結構見つかって、多くの人が戸惑っている様子が伺える。

OBJECTIVE C - Release in viewDidUnload and dealloc both? - efreedom

When should I release objects in -(void)viewDidUnload rather than in -dealloc? - Stack Overflow

答えはどれも先のまとめで示した通りで両方でメモリ解放することが推奨されている。


- - - -
今回の件はかなりショックだった。過去のプログラムを見直せねば。。

Xcode のマクロ定義

2010年10月7日木曜日 | Published in | 2 コメント

このエントリーをはてなブックマークに追加

Xcodeのマクロ定義は下記が詳しい。

【Xcode】設定しておくと便利なカスタマイズいろいろ | iphoneアプリで稼げるのか

コピー元
/Developer/Applications/Xcode.app/Contents/PlugIns/
  TextMacros.xctxtmacro/Contents/Resources/ObjectiveC.xctxtmacro

これを下記へコピーする。
~/Library/Application Support/Developer/Shared/Xcode/Specifications/

後はこれに追加していけばよい。

以下はプロパティ定義文を挿入するマクロ。
       {
            Identifier = objc.property1;
            BasedOn = objc;
            IsMenuItem = NO;
            Name = "@property retain";
            TextString = "@property (nonatomic, retain) <#!statements!#>";
            CompletionPrefix = ppr;
            OnlyAtBOL = YES;
       },

        {
            Identifier = objc.property2;
            BasedOn = objc;
            IsMenuItem = NO;
            Name = "@property assign";
            TextString = "@property (nonatomic, assign) <#!statements!#>";
            CompletionPrefix = ppa;
            OnlyAtBOL = YES;
       },

        {
            Identifier = objc.property3;
            BasedOn = objc;
            IsMenuItem = NO;
            Name = "@property outlet";
            TextString = "@property (nonatomic, retain) IBOutlet <#!statements!#>";
            CompletionPrefix = ppo;
            OnlyAtBOL = YES;
       },

"ppo"と打って ESCキーを押すと下記のようになる。


同様に "ppr" "ppa" と打つと次のようになる。

なかなか便利。

人気の投稿(過去 30日間)