2012年5月29日火曜日

MapKitサンプル1 - 地図の表示と操作

前回の「CoreLocationサンプル3 - ジオコーディング」に続いて、今回は地図の表示と操作です。
やはり緯度・経度の座標情報には、地図がなければ分かりづらいです。

iOS Developer Libraryの「位置情報対応プログラミングガイド」(英語版はMaking Your Application Location-Aware)に沿って、サンプルを公開します。


注意点

MapKitでは、Googleマップを利用しています。そのため、Googleマップの利用規約に従う必要があります。
詳細は、Googleマップの利用規約(Google Maps Terms of Service)(全文)をご覧ください。


実装手順

地図の表示

  • 「MapKit.framework」「CoreLocation.framework」の追加
    1. 単に地図を表示するだけならCoreLocationは不要ですが、様々な処理をするのに必要となります。
  • ヘッダのインポート
    #import <MapKit/MapKit.h>
    #import <CoreLocation/CoreLocation.h>
  • (必要ならば)プロトコルの宣言
    @interface ViewController : UIViewController <MKMapViewDelegate>
    @end
  • MKMapViewの作成
    1. InterfaceBuilderで追加するか、以下のようにして作成します
    - (void)viewDidLoad
    {
        // 画面全体に表示
        MKMapView *mapView = [[[MKMapView alloc] initWithFrame:CGRectMake(0, 0, 320, 460)] autorelease];
        [self.view addSubview:mapView];
        
        // ユーザの現在位置を表示
        _mapView.showsUserLocation = YES;
        
        // 地図の種類をハイブリッドにする
        _mapView.mapType = MKMapTypeHybrid;
        
        // デバイスの向きに合わせて地図を回転
        [_mapView setUserTrackingMode:MKUserTrackingModeFollowWithHeading animated:YES];
    }


3種類の座標

MKMapKitでは、以下の3種類の座標情報を扱い、地図を表示したり、各種操作をしたりします。
  • 地図座標(Map coordinates)
    • CoreLocationでも扱った「緯度・経度」の情報です。「-90 <= 緯度 <= +90」「-180 <= 経度 <= +180」となります。
  • 地図点(Map points)
    • 「地図座標」と同様ですが、地図をx,yの座標で管理します。「0.0 <= x <= 268435456.0」「0.0 <= y <= 268435456.0」となります。
  • 点(Points)
    • 地図全体に付けられているものではなく、デバイスのMKMapViewに対しての座標です。「0 < x < 320」「0 < y < 460」となり、画面にはどこの場所の地図が表示されているかなどをしてしています。

それぞれの座標は相互に変換することができ、変換方法は下記のとおりです。
変換元変換先変換方法
地図座標- (CGPoint)convertCoordinate:(CLLocationCoordinate2D)coordinate toPointToView:(UIView *)view
- (CGRect)convertRegion:(MKCoordinateRegion)region toRectToView:(UIView *)view
地図座標地図点MKMapPoint MKMapPointForCoordinate(
CLLocationCoordinate2D coordinate
);
地図点地図座標CLLocationCoordinate2D MKCoordinateForMapPoint(
MKMapPoint mapPoint
);
MKCoordinateRegion MKCoordinateRegionForMapRect(
MKMapRect rect
);
地図点- (CGPoint)pointForMapPoint:(MKMapPoint)mapPoint
- (CGRect)rectForMapRect:(MKMapRect)mapRect
地図座標- (CLLocationCoordinate2D)convertPoint:(CGPoint)point toCoordinateFromView:(UIView *)view
- (MKCoordinateRegion)convertRect:(CGRect)rect toRegionFromView:(UIView *)view
地図点- (MKMapPoint)mapPointForPoint:(CGPoint)point
- (MKMapRect)mapRectForRect:(CGRect)rect

その他、プロパティや有用なメソッドが多いため、徐々に追記します。

サンプル

ソースコードはGitHubにて公開しています。
iOS-SampleCodes/MapKitSample-MapView - GitHub



次回

次回は、「MapViewサンプル2 - 注釈の表示」です。

CoreLocationサンプル3 - ジオコーディング

前回は「CoreLocationサンプル2 - ヘディングイベント」でした。今回は、「緯度・経度から住所」「住所から緯度・経度」に変換するジオコーディングについてです。

iOS Developer Libraryの「位置情報対応プログラミングガイド」(英語版はMaking Your Application Location-Aware)に沿って、サンプルを公開します。

注意点

ジオコーディングは、サーバにリクエストを送り、サーバ側で処理をするものです。そのため、過度にジオコーディング要求をしてはいけません。詳細は「位置情報対応プログラミングガイド」23ページ目「Geocoderオブジェクトについて」をご覧ください。
また、MapKitを利用した逆ジオコーディングは、Googleマップの利用規約により、必ずGoogleマップと組み合わせなければなりません。他の会社の地図サービスなどに出力したりすることはできません。(Googleマップの利用規約(section 10.12)
詳細は、Googleマップの利用規約(Google Maps Terms of Service)(全文)をご覧ください。


正ジオコーディングと逆ジオコーディング

逆ジオコーディングはiOS 3.0以降ですが、正ジオコーディングはiOS 5.0以降でなければ使えません。
概要フレームワーク
正ジオコーディング「住所」→「緯度・経度」CoreLocation【iOS 5.0以降】
逆ジオコーディング「緯度・経度」→「住所」MapKit【iOS 3.0以降】【Deprecated in iOS 5.0
CoreLocation【iOS 5.0以降】
iOS 4.3以前にも対応させる必要があるならば、MapKitを使うことになり、正ジオコーディングは使えません。

実装手順

正ジオコーディング(CoreLocation)

  1. 「CoreLocation.framework」の追加
  2. ヘッダのインポート
    #import <CoreLocation/CoreLocation.h>
  3. 正・逆ジオコーディングの開始
    // インスタンスの生成
    CLGeocoder *geocoder = [[[CLGeocoder alloc] init] autorelease];
    
    // 正ジオコーディングの開始
    [geocoder geocodeAddressString:@"住所".text completionHandler:^(NSArray *placemarks, NSError *error) {
        if (error) {
            // エラーが発生している
        } else {
            for (CLPlacemark *p in placemarks) {
                // 複数の結果が存在する場合もある
            }
        }
    }];
    
    // 逆ジオコーディングの開始
    CLLocation *location = [[[CLLocation alloc] initWithLatitude:37.332708 longitude:-122.030336] autorelease];
    [geocoder reverseGeocodeLocation:location completionHandler:^(NSArray *placemarks, NSError *error) {
        if (error) {
            // エラーが発生している
        } else {
            if (0 < [placemarks count]) {
                // 結果はひとつしかない
            }
        }
    }];

MapKit.framework

  1. 「MapKit.framework」の追加
  2. ヘッダのインポート
    #import <MapKit/MapKit.h>
  3. MKReverseGeocoderDelegateプロトコルの宣言
    @interface ViewController : UIViewController <MKReverseGeocoderDelegate>
    @property (nonatomic, retain) MKReverseGeocoder *geocoder;
    @end
  4. 逆ジオコーディングの開始
    @synthesize geocoder = _geocoder;
    
    - (void)reverseGeocode:(CLLocationCoordinate2D)coord {
        self.geocoder = [[[MKReverseGeocoder alloc] initWithCoordinate:coord] autorelease];
        _geocoder.delegate = self;
        [_geocoder start];
    }
    ガイドラインに書かれている以下のような方法だと、MKReverseGeocoderオブジェクトが解放されずにiOS 5.0以降ではメモリリークします。(Xcode付属のサンプル「CurrentAddress」では正しく書かれています。)
    - (void)reverseGeocode:(CLLocationCoordinate2D)coord {
        // theGeocoderは解放されない
        MKReverseGeocoder *theGeocoder = [[MKReverseGeocoder alloc] initWithCoordinate:coord];
        theGeocoder.delegate = self;
        [theGeocoder start];
    }
  5. 逆ジオコーディングの受け取り
    /* エラー発生時 */
    - (void)reverseGeocoder:(MKReverseGeocoder *)geocoder didFailWithError:(NSError *)error
    {
        // ここで任意の処理
        NSLog(@"%s | %@", __PRETTY_FUNCTION__, error);
    }
    
    /* ジオコーディング成功時 */
    - (void)reverseGeocoder:(MKReverseGeocoder *)geocoder didFindPlacemark:(MKPlacemark *)placemark
    {
        // ここで任意の処理
    }
    

ジオコーディングの実行例

デバイスの言語の設定により、得られる情報が異なります。また、正ジオコーディング結果の緯度・経度も、regionも値が返ってくるわけでもないようです。

CoreLocationでの正ジオコーディングの例

住所緯度, 経度
東京タワー+35.658608, +139.745396
Apple Inc. 2 Infinite Loop+37.332708, -122.030336


MapKitでの逆ジオコーディング「東京タワー」の例(iOS 5.1)

プロパティ日本語環境英語環境
addressDictionary{
    City = "港区";
    Country = "日本";
    CountryCode = JP;
    FormattedAddressLines = (
        "日本",
        "東京都港区4丁目2−8"
    );
    State = "東京都";
    Street = "4丁目 2−8";
    SubLocality = "芝公園";
    SubThoroughfare = "2−8";
    Thoroughfare = "4丁目";
}
{
    City = Minato;
    Country = Japan;
    CountryCode = JP;
    FormattedAddressLines = (
        Japan,
        "Tokyo, Minato, Shibakoen, 4丁目2−8"
    );
    State = Tokyo;
    Street = "4丁目 2−8";
    SubLocality = Shibakoen;
    SubThoroughfare = "2−8";
    Thoroughfare = "4丁目";
}
administrativeArea東京都Tokyo
areasOfInterest(null)(null)
country日本Japan
inlandWater(null)(null)
locality港区Minato
name(null)(null)
ocean(null)(null)
postalCode(null)(null)
region(null)(null)
subAdministrativeArea(null)(null)
subLocality芝公園Shibakoen
subThoroughfare2−82−8
thoroughfare4丁目4丁目

MapKitでの逆ジオコーディング「Apple Inc.」の例(iOS 5.1)

プロパティ日本語環境英語環境
addressDictionary{
    City = "クパチーノ";
    Country = "アメリカ合衆国";
    CountryCode = US;
    FormattedAddressLines = (
        "2 インフィニート・ループ",
        "クパチーノ カリフォルニア 95014",
        "アメリカ合衆国"
    );
    State = "カリフォルニア";
    Street = "2 インフィニート・ループ";
    SubThoroughfare = 2;
    Thoroughfare = "インフィニート・ループ";
    ZIP = 95014;
}
{
    City = Cupertino;
    Country = "United States";
    CountryCode = US;
    FormattedAddressLines = (
        "2 Infinite Loop",
        "Cupertino, CA 95014",
        USA
    );
    State = California;
    Street = "2 Infinite Loop";
    SubThoroughfare = 2;
    Thoroughfare = "Infinite Loop";
    ZIP = 95014;
}
administrativeAreaカリフォルニアCalifornia
areasOfInterest(null)(null)
countryアメリカ合衆国United States
inlandWater(null)(null)
localityクパチーノCupertino
name(null)(null)
ocean(null)(null)
postalCode9501495014
region(null)(null)
subAdministrativeArea(null)(null)
subLocality(null)(null)
subThoroughfare22
thoroughfareインフィニート・ループInfinite Loop

CoreLocationでの逆ジオコーディング「東京タワー」の例(iOS 5.1)

プロパティ日本語環境英語環境
addressDictionary{
    City = "東京";
    Country = "日本";
    CountryCode = JP;
    FormattedAddressLines = (
        "東京タワー",
        "〒105-0011",
        "東京 港区",
        "芝公園 4丁目2番",
        "日本"
    );
    Name = "東京タワー";
    State = "東京";
    Street = "芝公園 4丁目2番";
    SubLocality = "芝公園";
    SubThoroughfare = "2番";
    Thoroughfare = "芝公園 4丁目2番";
    ZIP = "〒105-0011";
}
{
    City = Minato;
    Country = Japan;
    CountryCode = JP;
    FormattedAddressLines = (
        "Tokyo Tower",
        "〒105-0011",
        "Tokyo Minato",
        "2, Shibakoen 4-Chōme",
        Japan
    );
    Name = "Tokyo Tower";
    State = Tokyo;
    Street = "2, Shibakoen 4-Chōme";
    SubLocality = Shibakoen;
    SubThoroughfare = 2;
    Thoroughfare = "Shibakoen 4-Chōme";
    ZIP = "〒105-0011";
}
administrativeArea東京Tokyo
areasOfInterest("東京タワー")("Tokyo Tower")
country日本Japan
inlandWater(null)(null)
locality港区Minato
name東京タワーTokyo Tower
ocean(null)(null)
postalCode〒105-0011〒105-0011
region(identifier <+35.65860800,+139.74534650> radius 133.46) <+35.65860800,+139.74534650> radius 133.46m(identifier <+35.65860800,+139.74534650> radius 133.46) <+35.65860800,+139.74534650> radius 133.46m
subAdministrativeArea(null)(null)
subLocality芝公園Shibakoen
subThoroughfare2番2
thoroughfare芝公園 4丁目Shibakoen 4-Chōme

CoreLocationでの逆ジオコーディング「Apple Inc.」の例(iOS 5.1)

プロパティ日本語環境英語環境
addressDictionary{
    City = "クパチーノ";
    Country = "合衆国";
    CountryCode = US;
    FormattedAddressLines = (
        "Apple Inc.",
        "2 Infinite Loop",
        "Cupertino, CA 95014-2083",
        "合衆国"
    );
    Name = "Apple Inc.";
    PostCodeExtension = 2083;
    State = "カリフォルニア";
    Street = "2 Infinite Loop";
    SubAdministrativeArea = "サンタクララ";
    SubLocality = "Bay Area";
    SubThoroughfare = 2;
    Thoroughfare = "Infinite Loop";
    ZIP = 95014;
}
{
    City = Cupertino;
    Country = "United States";
    CountryCode = US;
    FormattedAddressLines = (
        "Apple Inc.",
        "2 Infinite Loop",
        "Cupertino, CA 95014-2083",
        "United States"
    );
    Name = "Apple Inc.";
    PostCodeExtension = 2083;
    State = California;
    Street = "2 Infinite Loop";
    SubAdministrativeArea = "Santa Clara";
    SubLocality = "Bay Area";
    SubThoroughfare = 2;
    Thoroughfare = "Infinite Loop";
    ZIP = 95014;
}
administrativeAreaカリフォルニアCalifornia
areasOfInterest("Apple Inc.")("Apple Inc.")
country合衆国United States
inlandWater(null)(null)
localityクパチーノCupertino
nameApple Inc.Apple Inc.
ocean(null)(null)
postalCode9501495014
region(identifier <+37.33100000,-122.03085000> radius 414.66) <+37.33100000,-122.03085000> radius 414.66m(identifier <+37.33100000,-122.03085000> radius 414.66) <+37.33100000,-122.03085000> radius 414.66m
subAdministrativeAreaサンタクララSanta Clara
subLocalityBay AreaBay Area
subThoroughfare22
thoroughfareInfinite LoopInfinite Loop



サンプル

ソースコードはGitHubにて公開しています。
  1. CoreLocationを利用
    1. MapKitを利用

      CoreLocationを利用したサンプル


      MapKitを利用したサンプル



      今回はサンプルでUITableViewやUIMapViewを利用していますが、残念ながらiOS 5.1ではUIScrollViewが内部でメモリリークしています。
      フォーラムでも報告されており「Developer Forums: iOS 5.1 libsystem_c.dylib leaks memory when UIScrollview scrolling hits a bound」、日本語の説明ですと「iOS 5.1 ではスクロールビューをスクロールするたびに少しずつメモリリークが起こる。 - 24/7 twenty-four seven」にも書かれております。
      そのため、UIScrollViewを継承しているUITableViewやUIMapViewでも同様にメモリリークしてしまいます。

      次回

      次回は、「MapKitサンプル1 - 地図の表示」です。

      2012年5月27日日曜日

      CoreLocationサンプル2 - ヘディングイベントを利用した画面の回転

      前回の「CoreLocationサンプル1 - 現在位置の取得と領域観測 」に続いて、今回はヘディングイベントです。前回と重複している部分もありますが、iOS Developer Libraryの「位置情報対応プログラミングガイド」(英語版はMaking Your Application Location-Aware)に沿って、サンプルを公開します。

      方向関連のイベント

      • ヘディング(heading)【iOS 4.0以降】
        • 磁力センサーを利用して、「北」がどちらにあるかを計測します。単純に「北」と言っても、以下の二種類があり、どちらを示すかで方角が少し変わってきます。ヘディングでは両方を扱うことができます。詳細は「真北 - Wikipedia」「真北とは? | 用語集とGISの使い方 | 株式会社パスコ」などをご覧ください。
          • 磁北(じほく/magnetic north): 地球の磁場上の北です。(普通の)コンパスなどを使った場合に示されるものとなります。デバイスに内蔵されている磁力センサーを使って磁北を求めることができます。
          • 真北(しんぽく/true north): 普段の生活で良く使われる地図上の北です。北極点へと向かう線となります。磁北と真北はズレている為、現在地を元にしてそのズレを修正するものです。つまり、磁力センサーと現在地情報の両方が必要となります。
      • コース(course)【iOS 2.2以降】

      標準アプリ「コンパス」でも「真北」と「磁北」を切り替えられます。


      実装手順

      1. 「CoreLocation.framework」の追加
      2. 必要に応じて、<project-name>-Info.plistファイルにキー「location-services」「magnetometer」「gps」を追加する。
        • ただし、現在位置の情報や磁力センサー、GPSが無くともアプリケーションが正常に動作する場合には追加すべきではありません。(UIRequiredDeviceCapabilitiesについての詳細は「iOSデバイス間の互換性まとめ」をご覧下さい。)
      3. ヘッダのインポート
        #import <CoreLocation/CoreLocation.h>
        
      4. CLLocationManagerDelegateプロトコルの宣言
        @interface ViewController : UIViewController <CLLocationManagerDelegate>
        @end
      5. (真北の情報が必要であれば)位置情報サービスの開始
      6. ヘディングイベントの開始
      7. ヘディングイベント情報の受け取り
      8. (不必要になったら)ヘディングイベント(と位置情報サービス)の停止

      磁北の取得

      1. ヘディングイベントの開始
        // プロパティではなくクラスメソッドです
        if ([CLLocationManager headingAvailable]) {
            // インスタンスを生成し、デリゲートの設定
            _manager = [[CLLocationManager alloc] init];
            _manager.delegate = self;
            _manager.headingFilter = kCLHeadingFilterNone;  // 更新頻度
            _manager.headingOrientation = CLDeviceOrientationPortrait;  // 基準とする向き
        
            [_locationManager startUpdatingHeading];
        }
        iOS 3.xにも対応させる必要がある場合はプロパティ【Deprecated in iOS 4.0】を使います。
        // インスタンスの生成
        _manager = [[CLLocationManager alloc] init];
        if (_manager.headingAvailable || [CLLocationManager headingAvailable]) {
            // 必要な設定をして、ヘディングイベントの開始
        }
      2. ヘディング情報の受け取り
        - (void)locationManager:(CLLocationManager *)manager didUpdateHeading:(CLHeading *)newHeading
        {
            // ここで任意の処理
            CLLocationDirection heading = newHeading.magneticHeading;
        }
        
      3. (不必要になったら)ヘディングイベントの停止
        if ([CLLocationManager headingAvailable]) {
            [_manager stopUpdatingHeading];
        }

      真北の取得

      1. ヘディングイベントの開始
        // 位置情報サービスの開始
        if ([CLLocationManager locationServicesEnabled]) {
            _manager.desiredAccuracy = kCLLocationAccuracyBest;     
            _manager.distanceFilter = kCLDistanceFilterNone;
            [_manager startUpdatingLocation];
        }
        
        // ヘディングイベントの開始
        if ([CLLocationManager headingAvailable]) {  // プロパティではなくクラスメソッドです
            // インスタンスを生成し、デリゲートの設定
            _manager = [[CLLocationManager alloc] init];
            _manager.delegate = self;
            _manager.headingFilter = kCLHeadingFilterNone;
            _manager.headingOrientation = CLDeviceOrientationPortrait;
        
            [_locationManager startUpdatingHeading];
        }
        
      2. ヘディングイベント情報の受け取り
        - (void)locationManager:(CLLocationManager *)manager didUpdateHeading:(CLHeading *)newHeading
        {
            // ここで任意の処理
            CLLocationDirection heading = newHeading.trueHeading;
        }
        
      3. (不必要になったら)ヘディングイベントと位置情報サービスの停止
        if ([CLLocationManager headingAvailable]) {
            [_manager stopUpdatingHeading];
            [_manager stopUpdatingLocation];
        }


      CLLocationManager

      デバイスが機能を備えているかどうか

      • ヘディングイベントを扱えるか【iOS 4.0以降】
        + (BOOL)headingAvailable __OSX_AVAILABLE_STARTING(__MAC_10_7,__IPHONE_4_0);
        
        iOS 3.xでも使いたい場合【Deprecated in iOS 4.0
        @property(readonly, nonatomic) BOOL locationServicesEnabled __OSX_AVAILABLE_BUT_DEPRECATED(__MAC_NA,__MAC_NA,__IPHONE_2_0,__IPHONE_4_0);
        デバイスに、磁力センサーの機能があるかを確認する。【iOS 4.0以降】

      ヘディングイベントの挙動を設定

      • 次回の更新までの角度【iOS 3.0以降】
        @property(assign, nonatomic) CLLocationDegrees headingFilter __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_3_0);
        // 定義
        typedef double CLLocationDegrees;
        
        指定の角度動いたらヘディングイベントを生成する。単位は度数。kCLHeadingFilterNoneを指定すると、角度ではなく、動く度にイベントが通知されるようになります。デフォルトは1度です。

      • デバイスで基準とする向き【iOS 4.0以降】
        @property(assign, nonatomic) CLDeviceOrientation headingOrientation __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_4_0);
        // 定義
        typedef enum {
            CLDeviceOrientationUnknown = 0,
            CLDeviceOrientationPortrait,
            CLDeviceOrientationPortraitUpsideDown,
            CLDeviceOrientationLandscapeLeft,
            CLDeviceOrientationLandscapeRight,
            CLDeviceOrientationFaceUp,
            CLDeviceOrientationFaceDown
        } CLDeviceOrientation;
        
        デバイスのどの向きが基準となるかを決める。例えば、ホームボタンを右にして横向きに持った状態で北を設定した場合は「CLDeviceOrientationLandscapeLeft」を指定します。デフォルトはCLDeviceOrientationPortrait。
        イメージ定数説明
        無し
        CLDeviceOrientationUnknown無視される
        CLDeviceOrientationPortrait画面上部を北とする
        CLDeviceOrientationPortraitUpsideDown画面下部を北とする
        CLDeviceOrientationLandscapeLeft画面右部を北とする
        CLDeviceOrientationLandscapeRight画面左部を北とする
        画面が表(上)
        CLDeviceOrientationFaceUp無視される
        画面が裏(下)
        CLDeviceOrientationFaceDown無視される

      利用開始

      • ヘディングイベント【iOS 3.0以降】
        - (void)startUpdatingHeading __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_3_0);

      • キャリブレーション表示を隠す【iOS 3.0以降】
        - (void)dismissHeadingCalibrationDisplay __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_3_0);
        周りに磁力の強いものがあったりすると、正しく磁力センサーが働かなくなってしまう。その場合は下図のように、iOSが自動的に「8の字に振ってください」とのメッセージを表示するのだが、それを隠す。このメソッドを利用しなくても、キャリブレーションが成功すれば自動的に隠れます。(locationManagerShouldDisplayHeadingCalibrationを使わない場合に使用するのだろうが、使いどころが不明です)

      利用停止

      • ヘディングイベント【iOS 3.0以降】
        - (void)stopUpdatingHeading __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_3_0);

      情報の取得

      • ヘディング情報の取得【iOS 4.0以降】
        @property(readonly, nonatomic) CLHeading *heading __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_4_0);
        
        最新のヘディング情報が格納されている。デリゲートを利用しない場合にも利用できる。

      CLLocationManagerDelegate

      情報の更新時

      • ヘディング情報の更新時【iOS 3.0以降】
        - (void)locationManager:(CLLocationManager *)manager
               didUpdateHeading:(CLHeading *)newHeading __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_3_0);
        ヘディング情報が更新された際に呼ばれる。
      • キャリブレーション表示をするかどうか【iOS 3.0以降】
        - (BOOL)locationManagerShouldDisplayHeadingCalibration:(CLLocationManager *)manager  __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_3_0);
        周りに磁力の強いものがあったりすると、正しく磁力センサーが働かなくなってしまう。その場合に、「8の字に振ってください」とのメッセージを表示するかどうかを返す。表示しないと磁力センサーの値がおかしいまま修正されません。
        デザイン的に表示してほしくない場合などにNOを返すと良いのか?(使いどころが不明です。)


      CLHeading

      ヘディング情報

      • 磁北の角度【iOS 3.0以降】
        @property(readonly, nonatomic) CLLocationDirection magneticHeading;
        // 定義
        typedef double CLLocationDirection;
        
        磁北の角度を取得する。0.0から359.9の値で、0(北),90(東),180(南),270(西)となる。
      • 真北の角度【iOS 3.0以降】
        @property(readonly, nonatomic) CLLocationDirection trueHeading;
        // 定義
        typedef double CLLocationDirection;
        
        真北の角度を取得する。0.0から359.9の値で、0(北),90(東),180(南),270(西)となる。

      取得した情報の精度

      • 精度【iOS 3.0以降】
        @property(readonly, nonatomic) CLLocationDirection headingAccuracy;
        // 定義
        typedef double CLLocationDirection;
        
        磁北の方角と、計測された磁力から導き出した方角とのズレを角度で示す。値が小さいほど正確な磁北を示す。しかし、キャリブレーションが正しく行われていない場合など、負の値になる。
      • 計測された磁力の数値【iOS 3.0以降】
        @property(readonly, nonatomic) CLHeadingComponentValue x;
        @property(readonly, nonatomic) CLHeadingComponentValue y;
        @property(readonly, nonatomic) CLHeadingComponentValue z;
        
        それぞれの方向の磁力センサーから得られた磁力をマイクロテスラで示す。-128から+128までの正規化された値になる。


      サンプル

      ソースコードはGitHubにて公開しています。
      iOS-SampleCodes/CoreLocationSample-HeadingRelated - GitHub

      「設定 > 位置情報サービス >システムサービス > コンパスの調整」について


      電池の持ちを良くするためとして、位置情報サービスのシステムサービスの全てをオフにしていました。ただ、「コンパスの調整」だけは意味が分からないままだったので、今回はこれが原因でハマりました。このスイッチの意味は、iPhoneのユーザーズガイドにも詳細な説明は書かれておらず、色々と調べても分からずに疑問だったのですが、磁力センサーを利用して方角の計測をするときに「磁北」だけではなく「真北」も自動的に計測するというもののようです。
      • コンパスの調整「オン」
        • 磁北だけではなく、真北も自動で計測する。その際「ステータスバーアイコン」も「オン」になっていれば、ステータスバーの右上に矢印を表示する。
      • コンパスの調整「オフ」
        • 磁北のみで、真北は"利用できない"

      つまり、「コンパスの調整」がオフのままだと、「真北」の利用が出来なくなります。その時の、標準アプリ「コンパス」の例です。真北の選択ができません。

      今回のサンプルの場合は以下の通りです。コンパスの針に見立てた「^」が北を示します。ボタンを押すことで、磁北と真北とを切り替えることもできます。ですが、位置情報サービス(startUpdatingLocation)をオンにしても、真北の情報(trueHeading)を得られませんでした。

      そこで、「コンパスの調整」を「オン」にすると真北が利用できるようになりました。

      下図のように、アプリの標準位置情報サービスはまだオンになっていないのに、バックグラウンドで位置情報を取得し、真北を計算しています。この時表示されている矢印は「コンパスの調整」が自動的に位置情報を取得して真北を計算しているようです。それを表示するかしないかの判定が「ステータスバーアイコン」ということになります。
      上の方で、真北を得るためには「startUpdatingLocation」と「startUpdatingHeading」の両方が必要だと書いたのですが、位置情報サービスを開始しても真北を得られないとなると、それは困ります。ご存知の方、ご教示いただけると幸いです。

      「磁北」と「真北」の違い。若干、示す角度が異なっています。

      iPod touch(第4世代)では、磁力センサーがないため、電子コンパスとして利用できません。


      次回

      次回は、「CoreLocationサンプル3 - ジオコーディング」です。


      2012年5月26日土曜日

      CoreLocationサンプル1 - 現在位置の取得と領域観測

      今回もiOSデバイスが持っている機能を利用するものとして、位置情報サービスについて進めてゆきます。

      iOS Developer Libraryの「位置情報対応プログラミングガイド」(英語版はMaking Your Application Location-Aware)に沿って、サンプルを公開します。

      注意点

      カメラ・加速度センサー・ジャイロスコープ・磁力センサーなどのハードウェアを利用する場合とは、以下の点で異なります。
      1. 位置情報サービスを利用する場合は、ユーザに確認を取らなければならない
        • 確認用のダイアログはフレームワークが自動的に表示してくれます。必要になる直前に確認するようにしなければ、ユーザが「なぜその機能が必要なのか?」という懸念をいだくことがあります。
      2. 機内モードなど、デバイスが位置情報を利用できない場合がある
        • 一度でも位置情報サービスの取得に失敗すると次回も失敗する可能性が高くなるため、電池のことを考えて、しばらく休止するなどの処理が必要になります。
      3. ユーザが意図的に機能をオフにしているかもしれない
        • 使って欲しいがゆえに何度も確認メッセージを表示したりするのは、ユーザが不快に感じる可能性があります。位置情報サービスが必須なアプリでなければ、表示方法を検討すべきです。


      ユーザの位置情報の利用

      単純にユーザの位置情報を利用するだけでも、以下の3種類が用意されています。
      • 標準位置情報サービス(Standard Location Service)【iOS 全バージョン】
        • 携帯電話基地局・Wi-Fiスポット・GPSを組み合わせて現在地を取得します。携帯電話機能やGPSが付いていないデバイスであってもOS側で適切に判断して使用するハードウェアを選択します。
        • 基地局・Wi-Fi・GPSのいずれかの情報を取得できた順に利用します。そのため、検出直後などは精度が悪いことがありますが、徐々に高精度になります。総合すると、最も正確に現在位置を検出できるが、最終的な検出までに時間がかかります。また電池の消費量も最も多くなります。バックグラウンドや終了した時に位置情報を取得することはできません。
        • 例外として、<project-name>-Info.plistのUIBackgroundModesキーに「location」を追記すればバックグラウンドでも動作させ続けることができますが、電池を大量に消費します。(詳細は、「iOSアプリケーションプログラミングガイド」の62ページ目「ユーザの位置情報の追跡」を参照してください。)
      • 大幅変更位置情報サービス(Significant-Change Location Service)【iOS 4.0以降】
        • 携帯電話基地局を利用し、大まかな現在地を取得します。大まかとは言っても通常利用するには十分な精度で、現在位置の検出も早く、省電力です。そして、アプリがバックグラウンド動作中や終了していてもアプリを復帰させることができます。しかし、基地局を利用するため、iPhoneやiPad WiFi+Cellular(3G)などでしか利用できません。
      • 領域観測(Region Monitoring)【iOS 4.0以降】
        • 指定された場所に近づいた時や、出て行く時を検出します。その際、アプリがバックグラウンド動作中や終了していても設定した条件でアプリを復帰させることができます。


      実装手順

      1. 「CoreLocation.framework」の追加
      2. 必要に応じて、<project-name>-Info.plistファイルにキー「location-services」「gps」を追加する。
        • ただし、現在位置の情報やGPSが無くともアプリケーションが正常に動作する場合には追加すべきではありません。(UIRequiredDeviceCapabilitiesについての詳細は「iOSデバイス間の互換性まとめ」をご覧下さい。)
      3. ヘッダのインポート
        #import <CoreLocation/CoreLocation.h>
        
      4. CLLocationManagerDelegateプロトコルの宣言
        @interface ViewController : UIViewController <CLLocationManagerDelegate>
        @end
      5. 位置情報サービスの開始
      6. 位置情報の受け取り
      7. (不必要になったら)位置情報サービスの停止

      標準位置情報サービス

      1. 位置情報サービスの開始
        // プロパティではなくクラスメソッドです
        if ([CLLocationManager locationServicesEnabled]) {
            // インスタンスを生成し、デリゲートの設定
            _manager = [[CLLocationManager alloc] init];
            _manager.delegate = self;
            
            // 取得精度
            _manager.desiredAccuracy = kCLLocationAccuracyBest;     
            // 更新頻度(メートル)
            _manager.distanceFilter = kCLDistanceFilterNone;
            // サービスの開始
            [_manager startUpdatingLocation];
        }
        iOS 3.xにも対応させる必要がある場合はプロパティ【Deprecated in iOS 4.0】を使います。
        // インスタンスの生成
        _manager = [[CLLocationManager alloc] init];
        if (_manager.locationServicesEnabled || [CLLocationManager locationServicesEnabled]) {
            // 必要な設定をして、サービスの開始
        }
      2. 位置情報の受け取り(標準位置情報サービス・大幅変更位置情報サービスで共通)
        // 標準位置情報サービス・大幅変更位置情報サービスの取得に成功した場合
        - (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation
        {
            // ここで任意の処理
            NSLog(@"%s | %@, %@", __PRETTY_FUNCTION__, newLocation, oldLocation);
        }
        
        // 標準位置情報サービス・大幅変更位置情報サービスの取得に失敗した場合
        - (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error {
            // ここで任意の処理
            NSLog(@"%s | %@", __PRETTY_FUNCTION__, error);
        
            if ([error code] == kCLErrorDenied) {
                // 位置情報の利用が拒否されているので停止
                [manager startUpdatingLocation];
            }
        }
        
      3. (不必要になったら)位置情報サービスの停止
        if ([CLLocationManager locationServicesEnabled]) {
            [_manager stopUpdatingLocation];
        }

      大幅変更位置情報サービス

      1. 位置情報サービスの開始
        // プロパティではなくクラスメソッドです
        if ([CLLocationManager locationServicesEnabled]) {
            // インスタンスを生成し、デリゲートの設定
            _manager = [[CLLocationManager alloc] init];
            _manager.delegate = self;
            
            // 使用可能かの確認
            if ([CLLocationManager significantLocationChangeMonitoringAvailable]) {
                // サービスの開始
                [_manager startMonitoringSignificantLocationChanges];
            }
        }
      2. 位置情報の受け取り(標準位置情報サービス・大幅変更位置情報サービスで共通)
        // 標準位置情報サービス・大幅変更位置情報サービスの取得に成功した場合
        - (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation
        {
            // ここで任意の処理
            NSLog(@"%s | %@, %@", __PRETTY_FUNCTION__, newLocation, oldLocation);
        }
        
        // 標準位置情報サービス・大幅変更位置情報サービスの取得に失敗した場合
        - (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error {
            // ここで任意の処理
            NSLog(@"%s | %@", __PRETTY_FUNCTION__, error);
        
            if ([error code] == kCLErrorDenied) {
                // 位置情報の利用が拒否されているので停止
                [manager stopMonitoringSignificantLocationChanges];
            }
        }
        
      3. (不必要になったら)位置情報サービスの停止
        if ([CLLocationManager locationServicesEnabled] && [CLLocationManager significantLocationChangeMonitoringAvailable]) {
            [_manager stopMonitoringSignificantLocationChanges];
        }

      領域観測

      1. 位置情報サービスの開始
        // プロパティではなくクラスメソッドです
        if ([CLLocationManager locationServicesEnabled]) {
            // インスタンスを生成し、デリゲートの設定
            _manager = [[CLLocationManager alloc] init];
            _manager.delegate = self;
            
            // 使用可能かの確認
            if ([CLLocationManager regionMonitoringAvailable] && [CLLocationManager regionMonitoringEnabled]) {
                // 設定したい場所の情報を作成
                CLLocationCoordinate2D location = CLLocationCoordinate2DMake(37.332708, -122.030336);
                // 半径、キー文字列をもとにオブジェクトを生成
                CLRegion *region = [[CLRegion alloc] initCircularRegionWithCenter:location radius:100 identifier:@"Apple"];
                // サービスの開始
                [_manager startMonitoringForRegion:region desiredAccuracy:kCLLocationAccuracyBest];
                // 不要なものを解放
                [region release];
            }
        }
      2. 位置情報の受け取り
        // 領域観測の登録に失敗した場合
        - (void)locationManager:(CLLocationManager *)manager monitoringDidFailForRegion:(CLRegion *)region withError:(NSError *)error
        {
            // ここで任意の処理
            NSLog(@"%s | %@, %@", __PRETTY_FUNCTION__, region, error);
        }
        
        // 指定した領域に入った場合
        - (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region
        {
            // ここで任意の処理
            NSLog(@"%s | %@", __PRETTY_FUNCTION__, region);
        }
        
        // 指定した領域から出た場合
        - (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region
        {
            // ここで任意の処理
            NSLog(@"%s | %@", __PRETTY_FUNCTION__, region);
        }
      3. (不必要になったら)位置情報サービスの停止
        if ([CLLocationManager locationServicesEnabled]) {
            if ([CLLocationManager regionMonitoringAvailable] && [CLLocationManager regionMonitoringEnabled]) {
                // (不必要になったら)位置情報サービスの停止
                for (CLRegion *region in _manager.monitoredRegions) {
                    // 登録してある地点を全て取得し、停止
                    [_manager stopMonitoringForRegion:region];
                }
            }
        }


      CLLocationManager

      ユーザによる位置情報サービスの許可について

      • 標準位置情報サービス・大幅変更位置情報サービスの「オン」「オフ」【iOS 4.0以降】
        + (BOOL)locationServicesEnabled __OSX_AVAILABLE_STARTING(__MAC_10_7,__IPHONE_4_0);
        iOS 3.xでも使いたい場合【Deprecated in iOS 4.0
        @property(readonly, nonatomic) BOOL locationServicesEnabled __OSX_AVAILABLE_BUT_DEPRECATED(__MAC_NA,__MAC_NA,__IPHONE_2_0,__IPHONE_4_0);
        「設定 > 位置情報サービス」の値を参照して、「オン」の場合"YES"を返す。位置情報サービスが「オフ」の場合は"NO"だが、以下の場合は"YES"となります。
        1. 機内モード(圏外)
        2. アプリ毎の位置情報サービスの設定が「オフ」
        3. 機能制限がかかっていても、位置情報サービスは「オン」

      • 領域観測の「オン」「オフ」【iOS 4.0以降】
        + (BOOL)regionMonitoringEnabled __OSX_AVAILABLE_STARTING(__MAC_10_7,__IPHONE_4_0);
        startMonitoringForRegionだけを実行してみたが、位置情報サービスを利用するかのダイアログが表示される。上記の「locationServicesEnabled」との違いがわからなかった。

      • 位置情報の利用可否についての詳細【iOS 4.2以降】
        + (CLAuthorizationStatus)authorizationStatus __OSX_AVAILABLE_STARTING(__MAC_10_7,__IPHONE_4_2);
        「設定 > 位置情報サービス」の値を参照して、以下の値を返す。
        typedef enum {
            kCLAuthorizationStatusNotDetermined = 0, // User has not yet made a choice with regards to this application
            kCLAuthorizationStatusRestricted,        // This application is not authorized to use location services.  Due
                                                     // to active restrictions on location services, the user cannot change
                                                     // this status, and may not have personally denied authorization
            kCLAuthorizationStatusDenied,            // User has explicitly denied authorization for this application, or
                                                     // location services are disabled in Settings
            kCLAuthorizationStatusAuthorized         // User has authorized this application to use location services
        } CLAuthorizationStatus;
        
        定数説明
        kCLAuthorizationStatusNotDeterminedアプリ起動後に位置情報サービスを「利用する」か「しない」かの選択をまだしていない状態。表示されたダイアログの「OK」を選択すると位置情報サービスが利用できるようになる。「許可しない」を選択すると位置情報サービスが使えず、有効にするためにはユーザが「設定 > 位置情報サービス」で有効にしなければならない。
        kCLAuthorizationStatusRestricted機能制限(ペアレントコントロール)により位置情報サービスの利用開始が許可されていない状態。機能制限を解除してもらわない限り、ダイアログも表示されず、位置情報サービスの設定や利用をすることができない。
        kCLAuthorizationStatusDeniedユーザが(意図的に)位置情報サービスの利用を拒否している状態。位置情報サービス全体のオフと、アプリ毎の位置情報サービスのオフの両方が含まれます。確認ダイアログが1回だけ表示され、以降は表示されなくなる。
        kCLAuthorizationStatusAuthorizedユーザにより位置情報サービスの利用が許可されている状態。
        それぞれの状態がどのような流れで遷移するのか
        もし位置情報サービスを利用しなければならないアプリの場合は、以下の方法を検討しても良いかもしれません。
        if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusDenied) {
            // UIAlertViewなどでメッセージを表示する
        }
        

      デバイスがサービスを使える機能を備えているかどうか

      • 大幅変更位置情報サービス【iOS 4.0以降】
        + (BOOL)significantLocationChangeMonitoringAvailable __OSX_AVAILABLE_STARTING(__MAC_10_7,__IPHONE_4_0)
        デバイスに、携帯電話基地局の電波を利用できる機能があるかを確認する。
      • 領域観測【iOS 4.0以降】
        + (BOOL)regionMonitoringAvailable __OSX_AVAILABLE_STARTING(__MAC_10_7,__IPHONE_4_0);
        デバイスに、携帯電話基地局の電波を利用できる機能があるかを確認する。

      標準位置情報サービスの挙動を設定(大幅変更位置情報サービスでは使われません)

      • 次回の更新までの距離【iOS 2.0以降】
        @property(assign, nonatomic) CLLocationDistance distanceFilter;
        // 定義
        typedef double CLLocationDistance;
        一定距離を動いたら位置更新イベントを生成するかを指定する。単位はメートル。kCLDistanceFilterNoneを指定すると、距離ではなく、動く度に位置情報イベントが通知されるようになります。デフォルトはkCLDistanceFilterNoneです。

      • 位置情報の精度【iOS 2.0以降】
        @property(assign, nonatomic) CLLocationAccuracy desiredAccuracy;
        // 定義
        typedef double CLLocationAccuracy;
        位置情報取得の精度を指定する。精度が良ければ良いほど正確だが、その分、電池の消費量も大きく増える。なお、距離はおおまかなものであり、保証されたものではありません。
        定数説明
        kCLLocationAccuracyBestForNavigation最高レベル。ナビアプリなどで使用されることを想定され、充電状態でのみ使用されるようになっている。【iOS 4.0以降】
        kCLLocationAccuracyBestiOS 2.0/3.0での最高レベル。(恐らく数m以内の誤差ですが、詳細は不明です)【iOS 2.0以降】
        kCLLocationAccuracyNearestTenMeters10m以内の誤差【iOS 2.0以降】
        kCLLocationAccuracyHundredMeters100m以内の誤差【iOS 2.0以降】
        kCLLocationAccuracyKilometer1km程度の誤差【iOS 2.0以降】
        kCLLocationAccuracyThreeKilometers3km程度の誤差【iOS 2.0以降】

      ダイアログメッセージ

      • メッセージの追加【iOS 3.2以降】
        @property(copy, nonatomic) NSString *purpose __OSX_AVAILABLE_STARTING(__MAC_10_7,__IPHONE_3_2);
        ダイアログメッセージの下部に文字列を追加する。もちろん、位置情報サービスの開始よりも先に代入しなければならない。

      位置情報サービスの利用開始

      • 標準位置情報サービス【iOS 全バージョン】
        - (void)startUpdatingLocation;
      • 大幅変更位置情報サービス【iOS 4.0以降】
        - (void)startMonitoringSignificantLocationChanges __OSX_AVAILABLE_STARTING(__MAC_10_7,__IPHONE_4_0);
      • 領域観測【iOS 4.0以降】
        - (void)startMonitoringForRegion:(CLRegion *)region
                         desiredAccuracy:(CLLocationAccuracy)accuracy __OSX_AVAILABLE_STARTING(__MAC_10_7,__IPHONE_4_0);
        - (void)startMonitoringForRegion:(CLRegion *)region __OSX_AVAILABLE_STARTING(__MAC_TBD,__IPHONE_5_0);
        

      位置情報サービスの利用停止

      • 標準位置情報サービス【iOS 全バージョン】
        - (void)stopUpdatingLocation;
      • 大幅変更位置情報サービス【iOS 4.0以降】
        - (void)stopMonitoringSignificantLocationChanges __OSX_AVAILABLE_STARTING(__MAC_10_7,__IPHONE_4_0);
      • 領域観測【iOS 4.0以降】
        - (void)stopMonitoringForRegion:(CLRegion *)region __OSX_AVAILABLE_STARTING(__MAC_10_7,__IPHONE_4_0);

      情報の取得

      • 現在値の取得【iOS 2.0以降】
        @property(readonly, nonatomic) CLLocation *location;
        最新の位置情報が格納されている。大幅変更位置情報サービスや領域観測をトリガーとしたアプリの再起動時や、デリゲートを利用しない場合にも使える。
      • 領域観測で登録できる地点の半径【iOS 4.0以降】
        @property (readonly, nonatomic) CLLocationDistance maximumRegionMonitoringDistance __OSX_AVAILABLE_STARTING(__MAC_10_7,__IPHONE_4_0);
        これよりも大きな半径を観測することはできない。
      • 領域観測に登録している地点一覧【iOS 4.0以降】
        @property (readonly, nonatomic) NSSet *monitoredRegions __OSX_AVAILABLE_STARTING(__MAC_10_7,__IPHONE_4_0);
        「startMonitoringForRegion」で登録した領域観測する地点の情報が格納されている。

      CLLocationManagerDelegate

      位置情報サービスの「オン」「オフ」の検知

      • 位置情報サービスの設定が変更された場合【iOS 4.2以降】
        - (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status __OSX_AVAILABLE_STARTING(__MAC_10_7,__IPHONE_4_2);
        
        位置情報サービスの設定が変更された場合に呼ばれる。
        定数説明
        kCLAuthorizationStatusNotDetermined位置情報のリセットをした場合など
        kCLAuthorizationStatusRestricted機能制限で位置情報サービスの利用を「オフ」から変更できないようにした場合
        kCLAuthorizationStatusDenied位置情報サービスを「オフ」にした場合
        kCLAuthorizationStatusAuthorized位置情報サービスを「オン」にした場合

      標準位置情報サービス・大幅変更位置情報サービス

      • 位置情報の更新時【iOS 2.0以降】
        - (void)locationManager:(CLLocationManager *)manager
         didUpdateToLocation:(CLLocation *)newLocation
         fromLocation:(CLLocation *)oldLocation;
        
        位置情報が更新された際に呼ばれる。newLocationの値はキャッシュされた古い情報かもしれないので、timestampを参照した方が良い。
        • バックグラウンドで動作中の場合
        • アプリが終了していた場合
          • 「application:didFinishLaunchingWithOptions」に「UIApplicationLaunchOptionsLocationKey」がセットされている。その後、「locationManager:didUpdateToLocation:fromLocation:」が呼ばれる。
            - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
            {
                ...(snip)...
                if ([launchOptions objectForKey:UIApplicationLaunchOptionsLocationKey] ) {
                    // 受信時の処理
                }
                ...(snip)...
            }

      • 位置情報の取得に失敗した場合【iOS 2.0以降】
        - (void)locationManager:(CLLocationManager *)manager
         didFailWithError:(NSError *)error;
        
        位置情報の取得に失敗した場合に呼ばれる。しょっちゅう失敗するものなので、必ず考慮しなければならない。なお、errorがkCLErrorDeniedの場合はユーザが意図的に拒否したものなので、必ず位置情報サービスの利用を停止しなければならない。


      領域観測

      • 領域に入った場合【iOS 4.0以降】
        - (void)locationManager:(CLLocationManager *)manager
         didEnterRegion:(CLRegion *)region __OSX_AVAILABLE_STARTING(__MAC_10_7,__IPHONE_4_0);
        
      • 領域から出た場合【iOS 4.0以降】
        - (void)locationManager:(CLLocationManager *)manager
         didExitRegion:(CLRegion *)region __OSX_AVAILABLE_STARTING(__MAC_10_7,__IPHONE_4_0);
        
      • 領域の登録に失敗した場合【iOS 4.0以降】
        - (void)locationManager:(CLLocationManager *)manager
         monitoringDidFailForRegion:(CLRegion *)region
         withError:(NSError *)error __OSX_AVAILABLE_STARTING(__MAC_10_7,__IPHONE_4_0);
        


      CLLocation

      座標情報

      • 緯度(latitude)・経度(longitude)【iOS 2.0以降】
        @property(readonly, nonatomic) CLLocationCoordinate2D coordinate;
        // 定義
        typedef struct {
            CLLocationDegrees latitude;
            CLLocationDegrees longitude;
        } CLLocationCoordinate2D;
        
        typedef double CLLocationDegrees;
        
      • 海抜高度【iOS 2.0以降】
        @property(readonly, nonatomic) CLLocationDistance altitude;
        
        マイナスの場合は海の中となる。

      取得した情報の精度

      • 緯度・経度【iOS 2.0以降】
        @property(readonly, nonatomic) CLLocationAccuracy horizontalAccuracy;
        デバイスが取得した位置情報の中には大幅に精度の悪いものもある。そのため、これでどの程度の精度のものかを比較することができる。
      • 海抜高度【iOS 2.0以降】
        @property(readonly, nonatomic) CLLocationAccuracy verticalAccuracy;
        リファレンスには「iOSデバイスでは常にマイナスの値となる」と書かれていますが、サンプルプログラムでは取得できています。(それが正しい値かは確認できていません)

      移動情報

      • 取得した時刻【iOS 2.0以降】
        @property(readonly, nonatomic) NSDate *timestamp;
        
        通常、位置情報の取得は時間が掛かるものなので、計測完了までの間にキャッシュを返すことがある。そのため、古い情報などは位置情報場大幅にずれていることもある。そのような位置情報を判別するために使われる。公式ドキュメントでは、15秒より古い情報は無視しています。

      • 距離【iOS 3.2以降】
        - (CLLocationDistance)distanceFromLocation:(const CLLocation *)location __OSX_AVAILABLE_STARTING(__MAC_10_6,__IPHONE_3_2);
        // 【Deprecated in iOS 3.2】
        - (CLLocationDistance)getDistanceFrom:(const CLLocation *)location __OSX_AVAILABLE_BUT_DEPRECATED(__MAC_NA,__MAC_NA,__IPHONE_2_0,__IPHONE_3_2);
        // 定義
        typedef double CLLocationDistance;
        
        2点間の距離を測る。ただし、高度は考慮しない。

      • 速度【iOS 2.2以降】
        @property(readonly, nonatomic) CLLocationSpeed speed __OSX_AVAILABLE_STARTING(__MAC_10_7,__IPHONE_2_2);
        // 定義
        typedef double CLLocationSpeed;
        
        前回の場所から現在の場所への移動距離・時間を使い、速度を取得する。ただし、厳格なものではない為、あくまでの目安。

      • 角度【iOS 2.2以降】
        @property(readonly, nonatomic) CLLocationDirection course __OSX_AVAILABLE_STARTING(__MAC_10_7,__IPHONE_2_2);
        // 定義
        typedef double CLLocationDirection;
        
        前回の場所から現在の場所への移動方向を角度で示す。0.0から359.9の値で、「0(北), 90(東), 180(南), 270(西)」となる。マイナス値が返ってきた場合は、正しく取得されていない。


      サンプル

      ソースコードはGitHubにて公開しています。
      iOS-SampleCodes/CoreLocationSample-CurrentLocation - GitHub

      大幅変更位置情報サービスでは、アプリが終了しても位置情報サービスを利用できることが売りなため、当然ですがアプリが終了しても位置情報サービスを利用し続けます。そのため、大幅変更位置情報サービスを手動でオン・オフできるようにしなければ、アプリを削除するまでずっとバックグラウンドで動作し続けます。今回はスイッチを設けて、オフにすればバックグラウンドでの動作を止められるようになっています。
      CoreLocationのサンプルを書いてらっしゃる方のプログラムを色々と探したのですが、残念ながら、いずれもバックグラウンドで動作させてしまうと、その後はアプリを削除する以外に位置情報サービスを無効にする方法がありませんでした。

      位置情報は、やはり地図が無いと分かりづらいですが、その分単純なソースとなっています。
      スクリーンショットの時間がバラバラになってしまいました。一番の問題はテストのしづらさでしょうか。いくつか試した結果、今回のサンプルの位置情報の精度は、標準アプリの「リマインド」と同じくらいの精度となっています。


      次回

      次回は、「CoreLocationサンプル2 - ヘディングイベントを利用した画面の回転」です。

      2012年5月20日日曜日

      リモートコントロールイベントサンプル - マルティメディアの操作

      前回は「モーションイベントサンプル3 - CoreMotionを使った各種センサーの利用」でした。
      iOS Developer Libraryの「iOSイベント処理ガイド」(英語版はEvent Handling Guide for iOS)に沿って、サンプルを公開します。今回が最後のイベント処理「リモートコントロールイベント」です。

      リモートコントロールイベント【iOS 4.0以降】

      「iOSイベント処理ガイド」の初回「マルチタッチイベントサンプル1 - Gesture Recognizer」にも紹介した図ですが、今回は「Remote-control events」のところになります。iPhoneに付属のイヤホンを使い、音楽プレイヤーの「一時停止」「再生」「次の曲へ」といった操作をすることができます。
      ドキュメントにも書かれていますが、音楽・映像だけではなく、「画像」などのマルチメディアを操作することを想定しているようです。ただ、音楽以外に使うのは馴染みもなく実用性のあるアプリは難しいかもしれません。

      実装の手順

      1. ファーストレスポンダになる
        - (BOOL)canBecomeFirstResponder
        {
            return YES;
        }
        
        - (void)viewDidAppear:(BOOL)animated
        {
            [super viewDidAppear:animated];
            [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
            
            [self becomeFirstResponder];
        }
        
      2. リモートコントロールイベントの処理
        - (void)remoteControlReceivedWithEvent:(UIEvent *)event
        {
            if (event.type == UIEventTypeRemoteControl) {
                switch (event.subtype) {
                    case UIEventSubtypeRemoteControlPlay:
                        // 再生
                        break;
                    case UIEventSubtypeRemoteControlPause:
                        // 一時停止
                        break;
                    case UIEventSubtypeRemoteControlStop:
                        // 停止
                        break;
                    case UIEventSubtypeRemoteControlTogglePlayPause:
                        // 「Now Playing」の再生・一時停止から呼ばれる
                        break;
                    case UIEventSubtypeRemoteControlNextTrack:
                        // 次の曲へ
                        break;
                    case UIEventSubtypeRemoteControlPreviousTrack:
                        // 前の曲へ
                        break;
                    case UIEventSubtypeRemoteControlBeginSeekingBackward:
                        // 巻き戻し開始
                        break;
                    case UIEventSubtypeRemoteControlEndSeekingBackward:
                        // 巻き戻し終了
                        break;
                    case UIEventSubtypeRemoteControlBeginSeekingForward:
                        // 早送り開始
                        break;
                    case UIEventSubtypeRemoteControlEndSeekingForward:
                        // 早送り終了
                        break;
                    default:
                          // UIEventSubtypeNone と UIEventSubtypeMotionShakeなので来ない
                        break;
                }
            }
        }
      3. 管理を終える場合、リモートコントロールイベントの停止
        - (void)viewWillDisappear:(BOOL)animated
        {
            [[UIApplication sharedApplication] endReceivingRemoteControlEvents];
            [self resignFirstResponder];
            
            [super viewWillDisappear:animated];
        }
        

      「Now Playing」コントロールとは

      リモートコントロールイベントは、イヤホンなどから操作することができるのですが、それらが無くともリモートコントロールイベントをアプリケーションに送ることができます。それが「Now Playing」コントロールです。音楽を再生すると、この「Now Playing」コントロールに表示されるようになります。
      ダブルタップでアプリ一覧を表示スワイプすると表示される


      サンプル

      ソースコードはGitHubにて公開しています。
      iOS-SampleCodes/RemoteControlSample - GitHub

      なお、サンプルに含めている音源は、「音楽素材/魔王魂」様のものを利用させていただいております。ゲーム音楽に使える音楽ばかりで格好良いです。

      起動時「Now Playing」に表示されます

      「再生」ボタンで再生「一時停止」ボタンで一時停止

      「次の曲」ボタン長押しで「早送り」の開始離すと「早送り」の停止