MKMapViewは注釈を表示するべき位置(緯度経度)をMKAnnotation採用オブジェクト(id<MKAnnotation>インスタンス)として保持します。

$テン*シー*シー-1

 でもって、例えばスクロールをして保持しているid<MKAnnotation>インスタンスが示す注釈の位置が画面内に入った時に、デリゲートの-mapView:viewForAnnotation:メソッドを呼んで、そのid<MKAnnotation>インスタンスに関連づけたMKAnnotationViewインスタンスを受け取り、これを画面上に表示するわけです。

$テン*シー*シー-2

 で、さらにスクロールしてid<MKAnnotation>インスタンスが画面外(ただしある程度のマージンがあるみたい)になると、注釈の表示に使われていたMKAnnotationViewインスタンスは画面上から取りはぶかれリサイクル用の倉庫に蓄えられる。
 こうして蓄えられたMKAnnotationViewインスタンスが、デリゲートの-mapView:viewForAnnotation:メソッド内で、MKMapViewのdequeueReusableAnnotationViewWithIdentifier:メッセージによって再利用される。

$テン*シー*シー-3

 こんなふうにMKAnnotationViewインスタンスをむやみに作って、メモリを浪費しない(画面用のオブジェクトはメモリ喰い)仕組みになってるんで、注釈(id<MKAnnotation>インスタンス)を100や200を追加しても平気なんじゃないかと…

 試しに1000個のピン(注釈)を落としてみる事にします。
 使うプロジェクトは、またまた(その2)で用意したKyotoMap-2.zip
 やる事は単純で、ピンを落としているところでループするだけです。
 位置は、元々追加してたid<MKAnnotation>インスタンスの緯度経度
35.0212466、135.7555968

を中心に
-0.05 ~ +0.049

 の範囲で散らばしてみます。

$テン*シー*シー-4

 利用する関数は
rand()

 ドリル本で紹介したし、ブログでも何度か紹介してるおなじみの関数です。
rand() % 100


0~99

の範囲でランダムな値が作成できるんで、ここから
(rand() % 100) - 50

とすることで
-50 ~ 49

の範囲の値になり、この値をfloat値にキャストして
100.0

で割るので、最終的に
-0.5 ~ 0.49

の範囲のfloat値になります。後はこの最大変化量0.99を欲しい変化量にするための係数をかけると
-0.05 ~ 0.049 (今回なら係数=0.1とする)

の範囲で変化する値が得られるわけです。
 でもって今回も
srand()

 は呼びません。srand()?な人はここを見るべし。
 srand()は実際にリリースするなら呼ぶべきだけど、デバッグ時はむしろrand()が毎回同じ値の方がありがたいんすよ。
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
// 1000個のピンを落とす
for (int i = 0; i < 1000; i++) {
MKPointAnnotation* pin = [[MKPointAnnotation alloc] init];
pin.coordinate = CLLocationCoordinate2DMake(
35.0212466 + (float)((rand() % 100) - 50) / 100.0 * 0.1,
135.7555968 + (float)((rand() % 100) - 50) / 100.0 * 0.1);
[_mapView addAnnotation:pin];
}
});

 Runするとピンが、どかっとでるわけだ。

$テン*シー*シー-5

 で、これが予想どおりにサクサク動くんだわけなんですよ。
 iPhone 4S実機でも表示までには時間がかかるだけで、一度出た後はズームもスクロールもサクサクなんですな。かなり最適化されてて、へたに自分で管理するよりMKMapViewに任せておくのが吉。

 ただ~、ズームインはいいんだけど、ズームアウトした時も1000個そのまま表示する事になるんで、かなりな密集地帯になる。
 これはキツイ。

$テン*シー*シー-6
ぐはっ

 ズームアウトして表示領域が大きくなった時もMKMapViewはおかまい無しで全部のピンを表示する。それが嫌ならプログラマ側でピンを間引いてやらないと駄目なんですな。
 もしくはピンを出すのをやめて、ここらへんにピンがあるんで、ズームインしたらピンが現れますよ~的なマークを表示するとかね。

$テン*シー*シー-7

 いずれにせよ、地図の表示領域が変化した事を知る必要があるわけでして…
 実を言うと、これが前回使ったMKMapViewDelegateの-mapView:regionDidChangeAnimated:メソッドの、本来の役割だったわけです。
 例えば
mapView.region.span.latitudeDelta > 0.02

ならmapViewにaddAnnotation:した各MKPointAnnotationを取り外し、特定間隔でMKPointAnnotationをまびいた別のMKAnnotation採用クラスのインスタンスをaddAnnotation:してやれば、緯度が0.02度より広い範囲を表示している時は、この代替用の注釈が表示されるようにできるわけです。

 この特定間隔でMKPointAnnotationをまびいた、別のMKAnnotation採用クラスは
@interface KMAreaAnnotation : MKPointAnnotation
@end

とします。でもって今回は、最初に作る1000個のMKPointAnnotationインスタンスをNSArrayインスタンスに収納させ
_annotations

という名のインスタンス変数として保持し、まびいたKMAreaAnnotationインスタンスは、同じく
_areaannotations

という名のインスタンス変数として保持することにしてみます。
 実装はこんな感じ。
@interface KMAreaAnnotation : MKPointAnnotation
@end
@implementation KMAreaAnnotation
@end

@interface KMViewController ()>MKMapViewDelegate< {
MKMapView* _mapView;

NSArray* _annotations;
NSArray* _areaannotations;
}
@end
・・・
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
// 1000個のピンを落とす
NSMutableArray* array = [NSMutableArray arrayWithCapacity:1000];
for (int i = 0; i < 1000; i++) {
MKPointAnnotation* pin = [[MKPointAnnotation alloc] init];
pin.coordinate = CLLocationCoordinate2DMake(
35.0212466 + (float)((rand() % 100) - 50) / 100.0 * 0.1,
135.7555968 + (float)((rand() % 100) - 50) / 100.0 * 0.1);
[_mapView addAnnotation:pin];
[array addObject:pin];

}

_annotations = array; // 1000個のMKPointAnnotationを持ったNSArray
// 特定間隔で、間引き
NSMutableDictionary* dic = [NSMutableDictionary
dictionaryWithCapacity:[_annotations count]];
for (MKPointAnnotation* pin in _annotations) {
NSString* key = [NSString stringWithFormat:@"%.2fx%.2f",
pin.coordinate.latitude / 2, pin.coordinate.longitude / 2];
KMAreaAnnotation* area = [dic valueForKey:key];
if (area == nil) {
area = [[KMAreaAnnotation alloc] init];
area.coordinate = pin.coordinate;
[dic setObject:area forKey:key];
}
}
_areaannotations = [dic allValues]; // 間引かれたKMAreaAnnotationを持ったNSArray
[self mapView:_mapView regionDidChangeAnimated:NO]; // ピンを画面に表示
});
・・・

 注釈をまびくためにNSMutableDictionaryを用意して、
NSString* key = [NSString stringWithFormat:@"%.2fx%.2f",
pin.coordinate.latitude / 2, pin.coordinate.longitude / 2];

としてキーを作ってるのがみそです。
 これで緯度経度は小数点第2位までに四捨五入されます。で、これをキーにしてNSMutableDictionaryにオブジェクトが存在するか問い合わせ、存在するならすでにその領域用のKMAreaAnnotationは登録済みとし、なにもせず、nilだったなら、あらためて-setObject:forKey:で登録する。小数点第2位の敷居でまびくわけですな。
 で、最後にNSMutableDictionaryから登録したすべてのKMAreaAnnotationインスタンスをNSArrayの形で取り出して_areaannotationsに設定。
 いろいろなやり方があると思うんで、そこらへんは自分の好きなようにやってください。

 あとは、地図の表示エリアが変わった時に呼ばれる-mapView:regionDidChangeAnimated:メソッドで_areaannotations、_annotationsを切り替えて登録。
- (void)mapView:(MKMapView *)mapView
regionDidChangeAnimated:(BOOL)animated
{
[mapView removeAnnotations:mapView.annotations]; // 全部取り外す
if (mapView.region.span.latitudeDelta > 0.02) {
[mapView addAnnotations:_areaannotations]; // _areaannotationsを追加
} else {
[mapView addAnnotations:_annotations]; // _annotationsを追加
}
}

 ただーし、これだけだとKMAreaAnnotationインスタンスにもMKPinAnnotationViewが利用される事になる(MKUserLocation「iOSアプリで地図を出そう(その3)」で体験したやつです)。
 なのでMKUserLocationの時と同じく、引数で受け取ったオブジェクトのクラスを調べ、KMAreaAnnotationクラスの時はMKAnnotationViewインスタンスを利用して、16 x 16のオレンジ色の矩形を表示するようにしました。
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id )annotation
{
if ([annotation isKindOfClass:[KMAreaAnnotation class]]) {
static NSString* Identifier = @"AreaAnnotationIdentifier";
MKAnnotationView* aView = (MKAnnotationView *)[mapView
dequeueReusableAnnotationViewWithIdentifier:Identifier];
if (aView == nil) {
aView = [[MKAnnotationView alloc] initWithAnnotation:annotation
reuseIdentifier:Identifier];
aView.bounds = CGRectMake(0,0,16, 16);
aView.backgroundColor = [UIColor orangeColor];
aView.canShowCallout = NO;
aView.draggable = NO;
return aView;
}
aView.annotation = annotation;
return aView;
}
static NSString* Identifier = @"PinAnnotationIdentifier";
・・・
}

 ま、これでRunすると、間引きはされてるし、ズームインして一定の表示領域になるとピンが現れるようになるんですが…
 スクロールのたびにピンが落ちてくる
はめになります。
removeAnnotations:mapView.annotations

して
removeAnnotations:

してりゃあたりまえ。その都度ピン追加アニメーションが走っちゃうわけですよ。
 今回ならmapView.region.span.latitudeDelta > 0.02の切り替わり時のみ、removeAnnotations:、removeAnnotations:の作業をおこなうようにすれば一件落着(というか、それが一番無駄がない)なんですが、今回はもうちょっと汎用性のあるやり方をやってみます。
 現在設定されているid<MKAnnotation>インスタンス群から、不要なものだけ取りはぶき、存在していなくて、これから必要なものだけ追加するようにしたらいいわけです。
 現在設定されているid<MKAnnotation>インスタンス群はMKMapViewのannotationsプロパティでわかるので、mapView.annotationsとこれから追加するNSArrayの内容を比較して
一致する:そのまま残す
mapView.annotations側だけ存在:削除する
追加するNSArray側だけ存在:追加する

というふうにやってみます。
 こういう時は、NSArrayじゃなくNSMutableSetを使うのが吉。NSMutableSetは収納物同士の引き算や足し算ができるんで、まずmapView.annotationsからsetWithArray:コンビニエンスコンストラクタでNSMutableSetインスタンスを作り、そこから、今回追加するNSArray(こっちはNSSetにしておく)と一致するものをとりはぶく。
 これで、削除するべきid<MKAnnotation>インスタンス群が特定できる。

 同じ要領で今度はNSMutableSetを追加するNSArrayから作り、mapView.annotationsに存在し、すでに登録済みのオブジェクトを追加対象からとりはぶく。
 これで、あらたに追加するべきid<MKAnnotation>インスタンス群が特定できる。

 あとはそれぞれに対しremoveAnnotations:とaddAnnotations:を呼び出せば、不要なものが削除され、新たに必要なものだけが追加されます。
 NSMutableSetインスタンスから収納物をNSArrayとして取り出すにはallObjectsメッセージを使えばいい。ここでは
[removeset allObjects]
と書かずにドット構文
removeset.allObjects
を使ってます。

- (void)mapView:(MKMapView *)mapView
regionDidChangeAnimated:(BOOL)animated
{

NSMutableSet* addset = nil;
if (mapView.region.span.latitudeDelta > 0.02) {
addset = [NSMutableSet setWithArray:_areaannotations];
} else {
addset = [NSMutableSet setWithArray:_annotations];
}
NSMutableSet* removeset = [NSMutableSet setWithArray:mapView.annotations];
NSSet* alreadyexistset = [removeset copy];
[removeset minusSet:addset]; // 追加対象側に存在するものは、削除対象からはぶく
[addset minusSet:alreadyexistset]; // すでにmapView.annotations側に
// 存在するものは追加対象からはぶく
[mapView removeAnnotations:removeset.allObjects]; // 削除
[mapView addAnnotations:addset.allObjects]; // 追加
}

 以上「表示領域の大きさで注釈を出したり消したりする」でした。

 注釈をランク付けして、ズームイン時はランクの高いものを残して取り外していくとか、ズームアウトでピンに変わるときは、ドロップアニメーションを止めるとか、いろいろ工夫すると面白いでしょう。
 そこらへんは各自でやってみてください。

------------
サンプルプロジェクト:KyotoMap-7.zip