2012年5月19日土曜日

モーションイベントサンプル3 - CoreMotionを使った各種センサーの利用

前回は「モーションイベントサンプル2 - UIAccelerometerを使った傾き検知」でしたが、「Deprecated in iOS 5.0」となっています。そこで今回は、iOS 4.0から用意されている「Core Motion」を利用します。
iOS Developer Libraryの「iOSイベント処理ガイド」(英語版はEvent Handling Guide for iOS)に沿って、サンプルを公開します。なお、日英版のどちらにも「磁力センサー」の利用について書かれていませんが、ここでは記述します。

注意点

今までのイベント処理では、シングルトンを利用してきましたが、Core Motionでは自分でインスタンスを生成することになります。ですが、複数のインスタンスを生成するとせっかくのデータにもズレが生じることがあります。そのため、インスタンスはひとつだけにすることが推奨されています。

Core Motion【iOS 4.0以降】

Core Motionには以下のクラスがあり、複数のセンサーが同時に扱えるようになっています。もちろん、データを取得するためには取得したいセンサーが内蔵されているデバイスでなければなりません。(各種デバイスの対応表については「iOSデバイス間の互換性まとめ」をご覧ください。)
クラス名概要対応OS
CMAccelerometerData加速度センサーのデータにアクセスできる。前回のUIAccelerometerDelegateの代わりとなります。X軸・Y軸・Z軸がどの方向に移動したかが分かります。iOS 4.0以降
CMGyroDataジャイロスコープのデータにアクセスできる。X軸・Y軸・Z軸を中心にしてどの方向に"回転"したかが分かります。iOS 4.0以降
CMMagnetometerData磁力センサーのデータにアクセスできる。X軸・Y軸・Z軸で計測される磁力の強さが分かります。iOS 5.0以降
CMDeviceMotion上記3つの、加速度センサー・ジャイロスコープ・磁力センサーのデータを統合して加工してくれます。iOS 4.0以降
CMAttitudeCMDeviceMotionに含まれるもので、デバイスの姿勢に関する情報をオイラー角(ロール・ピッチ・ヨー)や行列(マトリックス)の形にしてくれます。iOS 4.0以降
CMLogItemセンサーからの情報がいつ取得されたものかを示すタイムスタンプ。デバイスの起動時刻からの差分なので使い所は不明です。iOS 4.0以降
CMMotionManager上記のクラスを管理するクラス。このクラスを起点に様々なデバイスの情報を取り出します。iOS 4.0以降
各クラスの関係図

加速度センサー・磁力センサーで得られる情報の向きジャイロスコープで得られる情報の向き
Attitudeから得られるオイラー角。
pitch:縦方向、roll:横方向、yaw:デバイスの向きとなっている。


センサーからのデータ取得方法

センサーは定期的に情報を取得し続けるのですが、その情報をアプリケーションから利用するためには以下の2つの方法があります。
  1. プル型
    • センサーから取得されたデータを利用する為には、定期的に値を参照するようなプログラムを書かなければなりません。ドキュメントではこちらが推奨されています。しかし、OpenGLなどを使わない限りは、下のプッシュ型の方が使いやすいです。
  2. プッシュ型
    • 特定のハンドラを渡すと、センサーの値が更新される度にそのハンドラを実行してくれるものです。ハンドラはiOS 4.0から導入されたブロック構文となっています。

以下、利用方法について例を示しますが、いずれも共通部分は以下の通りです。
  1. 「CoreMotion.framework」を追加
  2. 必要に応じて、<project-name>-Info.plistファイルにキー「accelerometer」「gyroscope」「magnetometer」を追加する
    • ただし、例えセンサーが無くても他の機能で補うことができ、アプリケーションが正常に動作する場合には追加すべきではありません。(UIRequiredDeviceCapabilitiesについての詳細は「iOSデバイス間の互換性まとめ」をご覧下さい。)
  3. ヘッダの読み込み
    #import <CoreMotion/CoreMotion.h>
  4. インスタンスを生成し、適切な取得間隔の設定をして各種センサーを有効化
    イベント頻度(Hz)使用方法
    10–20デバイスの現在の向きを表すベクトルを確認するのに適しています。
    30-60ゲーム、またはリアルタイムユーザ入力用に加速度センサーを使用するその他のアプリケーションに適しています。
    70-100高い頻度でモーションを検出する必要があるアプリケーションに適しています。たとえば、この間隔を使って、ユーザがデバイスをたたいたり高速でゆすったりすることを検出する場合が考えられます。
  5. 各種センサーからのデータを取得
    • 上述のように、プル型とプッシュ型がある
  6. (不必要になったら)各種センサーを無効化

加速度センサー(Accelerometer)【iOS 4.0以降】

// インスタンスの生成
CMMotionManager *manager = [[CMMotionManager alloc] init];

// 現在、加速度センサー無しのデバイスは存在しないが念のための確認
if (manager.accelerometerAvailable) {
    // センサーの更新間隔の指定
    manager.accelerometerUpdateInterval = 0.01;  // 100Hz

    // ハンドラを指定
    CMAccelerometerHandler handler = ^(CMAccelerometerData *data, NSError *error) {
        double timestamp = data.timestamp;  // 更新時刻
        double x = data.acceleration.x;  // X軸: 加速度G
        double y = data.acceleration.y;  // Y軸: 加速度G
        double z = data.acceleration.z;  // Z軸: 加速度G

        // x, y, zの値を必要に応じて、ローパス・ハイパスなどのフィルタを適用する
    };

    // センサーの利用開始
    [manager startAccelerometerUpdatesToQueue:[NSOperationQueue currentQueue] withHandler:handler];
    
    // (不必要になったら)センサーの停止
    if (manager.accelerometerActive) {
        [manager stopAccelerometerUpdates];
    }
}

ジャイロスコープ(Gyroscope)【iOS 4.0以降】

// インスタンスの生成
CMMotionManager *manager = [[CMMotionManager alloc] init];

// ジャイロスコープ無しのデバイスもあるため、センサーの有無を確認
if (manager.gyroAvailable) {
    // センサーの更新間隔の指定
    manager.gyroUpdateInterval = 0.01;  // 100Hz
    // ハンドラを指定
    CMGyroHandler handler = ^(CMGyroData *data, NSError *error) {
        double timestamp = data.timestamp;  // 更新時刻
        double x = data.rotationRate.x;  // X軸: ラジアン/秒
        double y = data.rotationRate.y;  // Y軸: ラジアン/秒
        double z = data.rotationRate.z;  // Z軸: ラジアン/秒

        // x, y, zの値を適宜加工して利用する
    };

    // センサーの利用開始
    [manager startGyroUpdatesToQueue:[NSOperationQueue currentQueue] withHandler:handler];
        
    // (不必要になったら)センサーの停止
    if (manager.gyroActive) {
        [manager stopGyroUpdates];
    }
}

磁力センサー(Magnetometer)【iOS 5.0以降】

// インスタンスの生成
CMMotionManager *manager = [[CMMotionManager alloc] init];

// 磁力センサー無しのデバイスもあるため、センサーの有無を確認
if (manager.magnetometerAvailable) {
    // センサーの更新間隔の指定
    manager.magnetometerUpdateInterval = 0.01;  // 100Hz

    // ハンドラを指定
    CMMagnetometerHandler handler = ^(CMMagnetometerData *data, NSError *error) {
        double timestamp = data.timestamp;  // 更新時刻
        double x = data.magneticField.x;  // X軸: マイクロテスラ
        double y = data.magneticField.y;  // Y軸: マイクロテスラ
        double z = data.magneticField.z;  // Z軸: マイクロテスラ

        // x, y, zの値を適宜加工して利用する
    };

    // センサーの利用開始
    [manager startMagnetometerUpdatesToQueue:[NSOperationQueue currentQueue] withHandler:handler];
        
    // (不必要になったら)センサーの停止
    if (manager.magnetometerActive) {
        [manager stopMagnetometerUpdates];
    }
}

デバイスモーション(Device motion)【iOS 4.0以降】

// インスタンスの生成
CMMotionManager *manager = [[CMMotionManager alloc] init];

// 実質、ジャイロスコープの有無を確認
if (manager.deviceMotionAvailable) {
    // センサーの更新間隔の指定
    manager.deviceMotionUpdateInterval = 0.01;  // 100Hz

    // ハンドラを指定
    CMDeviceMotionHandler handler = ^(CMDeviceMotion *motion, NSError *error) {
        double timestamp = data.timestamp;  // 更新時刻

        /* 加速度センサー(ローパスフィルタ) */
        double gravityX = motion.gravity.x;  // X軸: 加速度G
        double gravityY = motion.gravity.y;  // Y軸: 加速度G 
        double gravityZ = motion.gravity.z;  // Z軸: 加速度G

        /* 加速度センサー(ハイパスフィルタ) */
        double userAccelerationX = motion.userAcceleration.x;  // X軸: 加速度G
        double userAccelerationY = motion.userAcceleration.y;  // Y軸: 加速度G
        double userAccelerationZ = motion.userAcceleration.z;  // Z軸: 加速度G

        /* ジャイロスコープ */
        double x = motion.rotationRate.x;  // X軸: ラジアン/秒
        double y = motion.rotationRate.y;  // Y軸: ラジアン/秒 
        double z = motion.rotationRate.z;  // Z軸: ラジアン/秒

        /* 磁力センサー */
        /* 下記の startDeviceMotionUpdatesUsingReferenceFrame で以下の値を指定した場合のみ取得可能
                - CMAttitudeReferenceFrameXArbitraryCorrectedZVertical
                - CMAttitudeReferenceFrameXMagneticNorthZVertical
                - CMAttitudeReferenceFrameXTrueNorthZVertical
           startDeviceMotionUpdatesToQueue や CMAttitudeReferenceFrameXArbitraryZVertical を指定した場合は取得できない */
        double x = motion.magneticField.x;  // X軸: マイクロテスラ
        double y = motion.magneticField.y;  // Y軸: マイクロテスラ
        double z = motion.magneticField.z;  // Z軸: マイクロテスラ
        CMMagneticFieldCalibrationAccuracy accuracy = motion.magneticField.accuracy;  // 磁力の強さ

        /* CMAttitude */
        double roll = motion.attitude.roll;  // Y軸中心のラジアン角: -π〜π(-180度〜180度)
        double pitch = motion.attitude.pitch; // X軸中心のラジアン角: -π/2〜π/2(-90度〜90度)
        double yaw = motion.attitude.yaw;  // Z軸中心のラジアン角: -π〜π(-180度〜180度)
    };

    /* 以下、どちらかの方法でセンサーの利用開始 */
    // 特に何も調整せずに開始
    [manager startDeviceMotionUpdatesToQueue:[NSOperationQueue currentQueue] withHandler:handler];
    // Z軸を鉛直として、X軸を横とする。その際、GPSと電子コンパスを利用して、X軸を"真北"に設定
    [manager startDeviceMotionUpdatesUsingReferenceFrame:CMAttitudeReferenceFrameXTrueNorthZVertical toQueue:[NSOperationQueue currentQueue] withHandler:handler];

    // (不必要になったら)センサーの停止
    if (manager.deviceMotionActive) {
        [manager stopMagnetometerUpdates];
    }
}
CMDeviceMotionにのみ、センサーの開始時に特別な初期化方法があります。【iOS 5.0以降】
定数初期化の方法magneticFieldの利用
CMAttitudeReferenceFrameXArbitraryZVerticalZ軸を鉛直として、X軸を横とする(FaceUp)×
CMAttitudeReferenceFrameXArbitraryCorrectedZVertical上記に加えて、電子コンパスで位置を修正する(ただし、上記のものよりもCPUを消費する)◯
CMAttitudeReferenceFrameXMagneticNorthZVerticalZ軸を鉛直として、X軸を横とする。その際、電子コンパスを利用して、X軸を"磁北"に設定◯
CMAttitudeReferenceFrameXTrueNorthZVerticalZ軸を鉛直として、X軸を横とする。その際、GPSと電子コンパスを利用して、X軸を"真北"に設定◯

利用方法のまとめ

・センサーの有無を確認

// 加速度センサー
@property(readonly, nonatomic, getter=isAccelerometerAvailable) BOOL accelerometerAvailable;
// ジャイロスコープ
@property(readonly, nonatomic, getter=isGyroAvailable) BOOL gyroAvailable;
// 磁力センサー
@property(readonly, nonatomic, getter=isMagnetometerAvailable) BOOL magnetometerAvailable NS_AVAILABLE(NA,5_0);
// デバイスモーション
@property(readonly, nonatomic, getter=isDeviceMotionAvailable) BOOL deviceMotionAvailable;

・センサーで計測中かどうか

// 加速度センサー
@property(readonly, nonatomic, getter=isAccelerometerActive) BOOL accelerometerActive;
// ジャイロスコープ
@property(readonly, nonatomic, getter=isGyroActive) BOOL gyroActive;
// 磁力センサー
@property(readonly, nonatomic, getter=isMagnetometerActive) BOOL magnetometerActive NS_AVAILABLE(NA,5_0);
// デバイスモーション
@property(readonly, nonatomic, getter=isDeviceMotionActive) BOOL deviceMotionActive;

・更新間隔の指定(秒数)

// 加速度センサー
@property(assign, nonatomic) NSTimeInterval accelerometerUpdateInterval;
// ジャイロスコープ
@property(assign, nonatomic) NSTimeInterval gyroUpdateInterval;
// 磁力センサー
@property(assign, nonatomic) NSTimeInterval magnetometerUpdateInterval NS_AVAILABLE(NA,5_0);
// デバイスモーション
@property(assign, nonatomic) NSTimeInterval deviceMotionUpdateInterval;

・センサーの利用開始(プル型)

// 加速度センサー
- (void)startAccelerometerUpdates;
// ジャイロスコープ
- (void)startGyroUpdates;
// 磁力センサー
- (void)startMagnetometerUpdates NS_AVAILABLE(NA,5_0);
// デバイスモーション
- (void)startDeviceMotionUpdates;
- (void)startDeviceMotionUpdatesUsingReferenceFrame:(CMAttitudeReferenceFrame)referenceFrame NS_AVAILABLE(NA,5_0);

・センサーの利用開始(プッシュ型)

// 加速度センサー
- (void)startAccelerometerUpdatesToQueue:(NSOperationQueue *)queue withHandler:(CMAccelerometerHandler)handler;
// ジャイロスコープ
- (void)startGyroUpdatesToQueue:(NSOperationQueue *)queue withHandler:(CMGyroHandler)handler;
// 磁力センサー
- (void)startMagnetometerUpdatesToQueue:(NSOperationQueue *)queue withHandler:(CMMagnetometerHandler)handler NS_AVAILABLE(NA,5_0);
// デバイスモーション
- (void)startDeviceMotionUpdatesToQueue:(NSOperationQueue *)queue withHandler:(CMDeviceMotionHandler)handler;
- (void)startDeviceMotionUpdatesUsingReferenceFrame:(CMAttitudeReferenceFrame)referenceFrame toQueue:(NSOperationQueue *)queue withHandler:(CMDeviceMotionHandler)handler NS_AVAILABLE(NA,5_0);

・ハンドラ

// 加速度センサー
typedef void (^CMAccelerometerHandler)(CMAccelerometerData *accelerometerData, NSError *error);
// ジャイロスコープ
typedef void (^CMGyroHandler)(CMGyroData *gyroData, NSError *error);
// 磁力センサー
typedef void (^CMMagnetometerHandler)(CMMagnetometerData *magnetometerData, NSError *error) NS_AVAILABLE(NA,5_0);
// デバイスモーション
typedef void (^CMDeviceMotionHandler)(CMDeviceMotion *motion, NSError *error);

・データの取得

// 加速度センサー
@property(readonly) CMAccelerometerData *accelerometerData;
// ジャイロスコープ
@property(readonly) CMGyroData *gyroData;
// 磁力センサー
@property(readonly) CMMagnetometerData *magnetometerData NS_AVAILABLE(NA,5_0);
// デバイスモーション
@property(readonly) CMDeviceMotion *deviceMotion;

・センサーの利用停止

// 加速度センサー
- (void)stopAccelerometerUpdates;
// ジャイロスコープ
- (void)stopGyroUpdates;
// 磁力センサー
- (void)stopMagnetometerUpdates NS_AVAILABLE(NA,5_0);
// デバイスモーション
- (void)stopDeviceMotionUpdates;


サンプル

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

起動時iPodなどでは
一部正しくセンサー情報が取得できないが、動作します


次回

次は「iOSイベント処理ガイド」の最後で、「リモートコントロールイベントサンプル - マルティメディアの操作」です。

2012年5月15日火曜日

モーションイベントサンプル2 - UIAccelerometerを使った傾き検知

今回もまた、前回の「モーションイベントサンプル1 - シェイクと回転検知」に引き続き、iOS Developer Libraryの「iOSイベント処理ガイド」(英語版はEvent Handling Guide for iOS)に沿って、サンプルを公開します。
今回は、「UIAccelerometerを介した加速度センサーイベントへのアクセス」です。

UIAccelerometerDelegate【iOS 2.0以降】【Deprecated in iOS 5.0】

前回は、デバイスの傾きを6方向で取得するだけでしたが、今回の「UIAccelerometer」を使うと任意の方向への傾きを計測することができます。文字通り加速度計です。しかし、残念ながら今回紹介するUIAccelerometerDelegateのメソッド「accelerometer:didAccelerate:」は「Deprecated in iOS 5.0」です。まだ使うことはできますが、次回説明予定の、iOS 4から導入された「Core Motion」を使うように推奨されています。(「モーションイベントサンプル3 - CoreMotionを使った各種センサーの利用」)

Core Motionの使い方は次に回して、今回はこのままUIAccelerometerによる加速度センサーの利用方法です。
  1. <project_name>-Info.plistファイルにキー「accelerometer」を追加する。(UIRequiredDeviceCapabilitiesについての詳細は「iOSデバイス間の互換性まとめ」をご覧下さい。)
  2. UIAccelerometerDelegateプロトコルを指定する
    @interface ViewController : UIViewController <UIAccelerometerDelegate>
    @end
    
  3. 加速度センサーからの情報を受け取る間隔を設定し、デリゲータに代入
    //Hz:取得する間隔
    const float accelerometerFrequency = 60.0;
    
    // シングルトンでインスタンスを取得
    UIAccelerometer* theAccelerometer = [UIAccelerometer sharedAccelerometer];
    
    // 間隔を秒に変換
    theAccelerometer.updateInterval = 1 / accelerometerFrequency;
    
    // デリゲータに代入することですぐに加速度センサーからの情報が取得できる
    theAccelerometer.delegate = self;
  4. メソッドを実装
    -(void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration
    {
        // 加速度センサーからの入力を受け取り処理する
        UIAccelerationValue x = acceleration.x;
        UIAccelerationValue y = acceleration.y;
        UIAccelerationValue z = acceleration.z;
    
        // x, y, zの値を利用する
    }
  5. (情報が不必要になったら)加速度センサーからの情報の受け取りを停止する
    [UIAccelerometer sharedAccelerometer].delegate = nil;
    

UIAccelerationValueはtypedefされているだけなのでdouble型です。
- UIAccelerometer.h
typedef double UIAccelerationValue;
そして、得られる数値は傾ける方向により「+」と「-」で表され、以下のような値になっています。静止している場合は「-1.0〜1.0」の範囲に収まります。

イメージXYZイメージXYZ
0.0-1.00.0
0.01.00.0
-1.00.00.0
1.00.00.0
画面が表(上)
0.00.0-1.0
画面が裏(下)
0.00.01.0

適切な更新間隔

上の例では、1秒間に60回の情報を取得するようにしていましたが、多いほど良いというわけではなく、電池のことも考えなければなりません。ドキュメント内では以下のように推奨されています。30fps,60fpsのゲームの場合はそれに合わせてそれぞれの30Hz,60Hzにすれば良いはずです。
イベント頻度(Hz)使用方法
10–20デバイスの現在の向きを表すベクトルを確認するのに適しています。
30-60ゲーム、またはリアルタイムユーザ入力用に加速度センサーを使用するその他のアプリケーションに適しています。
70-100高い頻度でモーションを検出する必要があるアプリケーションに適しています。たとえば、この間隔を使って、ユーザがデバイスをたたいたり高速でゆすったりすることを検出する場合が考えられます。

サンプル

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

何もフィルタをかけていない情報と、重力による影響のみを残したローパスフィルタと、その反対に重力による影響を取り除いたハイパスフィルタの動作を確認できます。

傾けるとUILabelが移動します。


注意点としては、移動可能な範囲を判定していないためどこままでも移動します。見失ってしまった場合はアプリを再起動することをおすすめします。


次回

最初にも書きましたが、UIAccelerometerDelegateの代わりに使用が推奨されている「Core Motion」を使い、「モーションイベントサンプル3 - CoreMoionを使った各種センサーの利用」を説明します。

2012年5月14日月曜日

モーションイベントサンプル1 - シェイクと回転検知

前回の「マルチタッチイベントサンプル2 - カスタムGesture Recognizer」に引き続き、iOS Developer Libraryの「iOSイベント処理ガイド」(英語版はEvent Handling Guide for iOS)に沿って、サンプルを公開します。
今回は、「シェイクモーションイベント」と「現在のデバイスの向きの取得」です。

シェイクモーションイベント【iOS 3.0以降】

文章の編集中などにデバイスをシェイクすると「取り消す」かどうかを聞かれますが、あれがシェイクモーションイベントです。
内部的には加速度センサーを利用してデバイスが振られたのかどうかを判定しています。しかし、プログラミングする際には加速度センサーを意識する必要はなく、タッチやスワイプなどと同様に「UIEvent」として同じように扱うことができます。
実装の手順は以下の通りです。
  1. ファーストレスポンダになる
    - (BOOL)canBecomeFirstResponder
    {
        return YES;
    }
    
    - (void)viewDidAppear:(BOOL)animated
    {
        [super viewDidAppear:animated];
        
        [self becomeFirstResponder];
    }
  2. モーションイベントを受けるメソッドを実装
    - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
    {
        // モーションイベントの開始時に処理される
    }
    
    - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
    {
        // モーションイベントの実行中に処理される
    }
    
    - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
    {
        // モーションイベントがキャンセルされた場合に処理される
        // (大抵の場合、デバイスが動いたが結局シェイクジェスチャーではなかったときに呼ばれる)
    }
    

現在のデバイスの向きの取得【iOS 2.0以降】

デバイスがどの方向を向いているのかを検知するものです。こちらも加速度センサーを使って判定しているのですが、モーションイベントと同様に、加速度センサーからの細かい情報を使うことなく実装できます。しかし、細かい傾きなどを知るためには加速度センサーなどを利用して実装する必要があるため、次回以降にサンプル実装します。
実装の手順は以下の通りです。
  1. UIDeviceクラスのbeginGeneratingDeviceOrientationNotificationsメソッドを実行し、加速度センサーを有効にする
    [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
  2. (オプション)UIDeviceOrientationDidChangeNotification通知の受け取りを登録する
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(orientationChanged:) name:UIDeviceOrientationDidChangeNotification object:nil];
    
  3. UIDeviceクラスのorientationプロパティから現在の傾きの情報を取得。その際、加速度センサーが有効になっていないと「UIDeviceOrientationUnknown」になります。
    [UIDevice currentDevice].orientation
  4. (オプション)UIDeviceOrientationDidChangeNotification通知の受け取りを登録していたら、削除する
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil];
    
  5. UIDeviceクラスのendGeneratingDeviceOrientationNotificationsメソッドを実行し、加速度センサーを無効にする
    [[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications];
    

shouldAutorotateToInterfaceOrientationとの違い

デバイスの回転に合わせて画面を回転させるメソッドとして「shouldAutorotateToInterfaceOrientation」があります。そのため、画面をデバイスに合わせて回転させたいのであれば、この「shouldAutorotateToInterfaceOrientation」を利用すべきです。
UIDeviceクラスのorientationプロパティでは、画面が表なのか裏なのかも判別することができます。その為、パズルゲームなどで画面上の一部のオブジェクトを回転させたり、デバイスが表や裏であるといったことを検知したい場合に有効です。
方向を定義する定数も若干異なっているため注意は必要です。以下のように定義されています。

- UIDevice.h (UIDeviceクラスのorientationプロパティで使われる)
typedef enum {
    UIDeviceOrientationUnknown,
    UIDeviceOrientationPortrait,            // Device oriented vertically, home button on the bottom
    UIDeviceOrientationPortraitUpsideDown,  // Device oriented vertically, home button on the top
    UIDeviceOrientationLandscapeLeft,       // Device oriented horizontally, home button on the right
    UIDeviceOrientationLandscapeRight,      // Device oriented horizontally, home button on the left
    UIDeviceOrientationFaceUp,              // Device oriented flat, face up
    UIDeviceOrientationFaceDown             // Device oriented flat, face down
}   UIDeviceOrientation;
- UIApplication.h (shouldAutorotateToInterfaceOrientationで使われる)
typedef enum {
    UIInterfaceOrientationPortrait           = UIDeviceOrientationPortrait,
    UIInterfaceOrientationPortraitUpsideDown = UIDeviceOrientationPortraitUpsideDown,
    UIInterfaceOrientationLandscapeLeft      = UIDeviceOrientationLandscapeRight,
    UIInterfaceOrientationLandscapeRight     = UIDeviceOrientationLandscapeLeft
} UIInterfaceOrientation;
イメージUIDeviceOrientationUIInterfaceOrientation
UIDeviceOrientationPortraitUIInterfaceOrientationPortrait
UIDeviceOrientationPortraitUpsideDownUIInterfaceOrientationPortraitUpsideDown
UIDeviceOrientationLandscapeLeftUIInterfaceOrientationLandscapeRight
UIDeviceOrientationLandscapeRightUIInterfaceOrientationLandscapeLeft
画面が表(上)
UIDeviceOrientationFaceUp無し
画面が裏(下)
UIDeviceOrientationFaceDown無し

サンプル

ソースコードはGitHubに公開してあります。
iOS-SampleCodes/Shake&DeviceOrientationNotification - GitHub

2つのサンプルを合わせただけなので簡単ですが、以下、サンプル画像です。
起動すると現在の画面の向きが表示されます。

回転させるとそのときの値に変わります。

シェイクすると、画面が赤くなり回転の判別が無効になります。その際、iPhoneの場合はバイブレーションで知らせます。もう一度振ると戻ります。

直接は関係ないですが、ユーザへのリアクションが分かりやすいように、シェイクモーションが認識されるとiPhoneの場合はバイブレーションで知らせるようにしています。方法は以下の通り簡単です。
  1. プロジェクトへ「AudioToolbox.framework」の追加
  2. ヘッダの読み込み
    #import <AudioToolbox/AudioServices.h>
    
  3. バイブレーションの実行
    // iPhoneのみだが、受け付けたらバイブレーションで知らせる
    AudioServicesPlayAlertSound(kSystemSoundID_Vibrate);
    


次回

次は、「モーションイベントサンプル2 - UIAccelerometerを使った傾き検知」です。

iOSデバイス間の互換性まとめ

iOSは既に数種類の端末が出ており、もちろん今後も出ることが期待されています。アプリの開発者としてはできる限り多くの人・端末で使って欲しいところですが、特定の機能を使わなければならないこともあります。そこで、iOSデバイスでの互換性について記述します。

デバイスと対応するOSの対応表

各デバイスの対応OS(2012/05/13現在) ※斜体はサポート終了
デバイス初期OS最終OS
iPod touch (第1世代)iPhone OS 1.1iPhone OS 3.1.3
iPod touch (第2世代)iPhone OS 2.2iOS 4.2.1
iPod touch (第3世代)iPhone OS 3.1iOS 5.1.1(現在)
iPod touch (第4世代)iOS 4.1/iOS 5iOS 5.1.1(現在)
iPhone (初代)iPhone OS 1.0iPhone iOS 3.1.3
iPhone 3GiPhone OS 2.0iOS 4.2.1
iPhone 3GSiPhone OS 3.0iOS 5.1.1(現在)
iPhone 4iOS 4iOS 5.1.1(現在)
iPhone 4SiOS 5iOS 5.1.1(現在)
iPadiPhone OS 3.2iOS 5.1.1(現在)
iPad 2iOS 4.3iOS 5.1.1(現在)
新しいiPadiOS 5.1iOS 5.1.1(現在)

OS別に分けた場合の対応デバイス(2012/05/13現在) ※斜体はサポート終了
対応のOSデバイス
iPhone OS 3.1.3まで
【3.2以降は非対応】
iPhone (初代)
iPod touch (第1世代)
iOS 4.2.1まで
【4.3以降は非対応】
iPhone 3G
iPod touch (第2世代)
iOS 5.1.1まで(現在)iPod touch (第3世代), iPod touch (第4世代)
iPhone 3G, iPhone 4, iPhone 4S
iPad, iPad2, 新しいiPad


UIRequiredDeviceCapabilitiesの値

iOS 3.0以降では、作成したアプリがどのような機能を使うのかということを「<project_name>-Info.plist」に必ず書かなければなりません。この値は、ユーザがApp Storeから誤ってダウンロードしないようにするためのものでもあります。そのため、条件をきつくすると本来ならば使えるデバイスで使えなくなってしまいますし、緩すぎるとアプリが正常に動作しなくなってしまいます。その機能が無くてもアプリケーションが動作する場合には記入すべきではありません。以下、記入することのできるキーとその説明です。
詳細は、以下のドキュメントに書かれています。

    iPod touchiPhoneiPad
    第1世代第2世代第3世代第4世代初代3G3GS3GS 中国版44SWi-FiWi-Fi+3G2 Wi-Fi2 Wi-Fi+3G新しい Wi-Fi新しい Wi-Fi+Cellular
    armv6◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯
    ARMv6に対応する(iOS 3.1以降)。iOSアプリケーション プログラミングガイド(英)の「浮動小数点演算の留意点(Floating-Point Math Considerations)」も参照。
    armv7--◯◯--◯◯◯◯◯◯◯◯◯◯
    ARMv7に対応する(iOS 3.1以降)。同上。
    telephony----◯◯◯◯◯◯------
    電話アプリを利用する。
    sms----◯◯◯◯◯◯------
    メッセージ(Message,iMessage)アプリを利用する。
    wifi◯◯◯◯◯◯◯-◯◯◯◯◯◯◯◯
    Wi-Fiを利用する。
    peer-peer-◯◯◯-◯◯◯◯◯◯◯◯◯◯◯
    Bluetoothを利用する(iOS 3.1以降)。
    bluetooth-le---------◯----◯◯
    Bluetooth low energy(Blutooth v4.0)を利用する(iOS 5.0以降)。
    still-camera---◯◯◯◯◯◯◯--◯◯◯◯
    カメラを利用する。
    front-facing-camera---◯----◯◯--◯◯◯◯
    フロントカメラを利用する。
    auto-focus-camera------◯◯◯◯----◯◯
    オートフォーカス(マクロ撮影)を利用する。
    camera-flash--------◯◯------
    カメラのLEDフラッシュを利用する。
    video-camera---◯--◯◯◯◯--◯◯◯◯
    ビデオ撮影を利用する。
    microphone-◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯
    内蔵マイクを利用する
    accelerometer◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯
    加速度センサーを利用する。
    gyroscope---◯----◯◯--◯◯◯◯
    3軸ジャイロを利用する。
    magnetometer------◯◯◯◯◯◯◯◯◯◯
    デジタルコンパスを利用する。
    gps-----◯◯◯◯◯-◯-◯-◯
    GPS(もしくはAssisted GPS)を利用する。同時にlocation-servicesも併記必須。
    location-services◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯
    現在地を利用する。GPSを利用しない場合はこれだけで良い。
    gamekit-◯◯◯--◯◯◯◯◯◯◯◯◯◯
    Game Centerを利用する(iOS 4.1以降)。
    opengles-1◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯
    OpenGLES 1.1を利用する。
    opengles-2--◯◯--◯◯◯◯◯◯◯◯◯◯
    OpenGLES 2.0を利用する。

    第1世代第2世代第3世代第4世代初代3G3GS3GS 中国版44SWi-FiWi-Fi+3G2 Wi-Fi2 Wi-Fi+3G新しい Wi-Fi新しい Wi-Fi+Cellular
    iPod touchiPhoneiPad
    Multitasking--◯◯--◯◯◯◯◯◯◯◯◯◯
    マルチタスクが可能である。
    Retina---◯----◯◯----◯◯
    Retinaディスプレイ搭載機である。
    下の方には、直接UIRequiredDeviceCapabilitiesとは関係ありませんが、デバイス間の比較のために「マルチタスク」「Retinaディスプレイ」を追記してあります。


    画面サイズ


    デバイス縦
    (ピクセル)
    横
    (ピクセル)
    アスペクト比
    iPod touch(第1世代)
    480
    320
    3:2
    iPod touch(第2世代)
    iPod touch(第3世代)
    iPhone (初代)
    iPhone 3G
    iPhone 3GS
    iPod touch(第4世代)
    960
    640
    iPhone 4
    iPhone 4S
    iPad (初代)
    1024
    768
    4:3
    iPad 2
    新しいiPad
    2048
    1536


    現在のiOSの割り合いについて

    そら案内 for iOS のアクセス解析結果に見るiOSの世界 | 関西/大阪のiPhone・iPadアプリ開発 feedtailor Inc. 社長ブログにあるように、アクセスされているiOSの割り合いを公開されているところもあります。非常にありがたいです。
    どんなOSでも当てはまりますが、公式のサポートが切れても使い続ける方は一定数おり、その割り合いが多い場合は動作検証した方が良い場合もあると思います。例えば、iPhone 3Gの場合重いからという理由でiOS 3.xを使っているかたも多いはずです。

    現状、多くのアプリの場合はiOS 4.3以上【iPod touch(第2世代), iPhone 3Gは対象外】としていることが多いように感じています。

    なお、以前SoftBankから公式に出された情報では、
    この度、ソフトバンクモバイルは、お客さまにより快適なモバイル環境をご提供するにあたり、音声ネットワークの最適化を2011年11月以降に実施いたします。 それに伴い、ご利用中のソフトウェアがiOS 4.2以降でない場合、一部音声品質に影響が出る可能性がございます。 iPhoneをコンピュータに接続し、iTunesをご利用の上、最新のソフトウェアへのアップデートをお願いします。
    となっており、3GSの場合でも多くはiOS 5以上にしているのではないかと(好意的に)思っています。

    しかし、iOS 5よりも前のデバイスは「PC FREE」ではないためアップデートしたくても分からない・できないという方もいるはずで、iOS 5以上しか対応しないというのでなければ、まだ数年はiOS 4.x(3Gを含めない場合はiOS 4.3)以上を対象にすることになると考えています。


    互換性を保つソースコードの書き方

    特定のバージョンのiOSの機能を利用する (iOS 2.0以降)

    ・コンパイル時に判定する方法
    以下の「Availability.h」「AvailabilityInternal.h」に定義されているマクロを利用します。
    - Availability.h
    #define __IPHONE_2_0     20000
    #define __IPHONE_2_1     20100
    #define __IPHONE_2_2     20200
    #define __IPHONE_3_0     30000
    #define __IPHONE_3_1     30100
    #define __IPHONE_3_2     30200
    #define __IPHONE_4_0     40000
    #define __IPHONE_4_1     40100
    #define __IPHONE_4_2     40200
    #define __IPHONE_4_3     40300
    #define __IPHONE_5_0     50000
    #define __IPHONE_5_1     50100
    #define __IPHONE_NA      99999  /* not available */
    
    - AvailabilityInternal.h
    __IPHONE_OS_VERSION_MIN_REQUIRED
    __IPHONE_OS_VERSION_MAX_ALLOWED
    

    「__IPHONE_OS_VERSION_MIN_REQUIRED」はターゲットの「Deployment Target」の値で変化します。

    if __IPHONE_OS_VERSION_MIN_REQUIRED >= 50000
        // iOS 5.0以上の処理
    else
        // iOS 5.0未満(iOS 4.3以下)の処理
    endif
    
    「__IPHONE_5_0」などのマクロも、もちろん利用できるが、Availability.hのコメントにも書かれている通り、古いSDKにはそのマクロが定義されていないので、「0」とみなされてしまう。そのため、以下の様なことになります。
    if __IPHONE_OS_VERSION_MIN_REQUIRED >= 0  // __IPHONE_5_0と書いたはずが、、、
        // iOS 5.0以上の処理のはずが、常にコンパイルされる
    endif
    

    ・実行時に判定する方法
    float version = [[[UIDevice currentDevice] systemVersion] floatValue];
    if (version >= 5.0) {
        // iOS 5.0以上の処理
    }


    メソッドの有無を確認

    if ([CLLocationManager respondsToSelector:@selector(locationServicesEnabled)]) {
        if (![CLLocationManager locationServicesEnabled]) {  // iOS 4.0以降はクラスメソッド
            // 何らかの処理
        }
    } else {
        if (!locationManager.locationServicesEnabled) {  // iOS 3.xはプロパティ
            // 何らかの処理
        }
    }

    2012年5月11日金曜日

    マルチタッチイベントサンプル2 - カスタムGesture Recognizer

    前回の「マルチタッチイベントサンプル1 - Gesture Recognizer」では、UIGestureRecognizerを使った基本的なジェスチャー認識を紹介しました。
    今回は、カスタムGesture Recognizerの作成として、UIGestureRecognizerのサブクラス「UIGestureRecognizerSubclass」のサンプル実装です。ドキュメントは「「iOSイベント処理ガイド」の45ページ目」になります。

    カスタムGesture Recognizer【iOS 3.2以降】

    用意されているUIGestureRecognizerクラスで十分なことが多いですが、それでもやはり欲しい機能を満たしてくれない場合もあります。そのため、UIGestureRecognizerのサブクラスであるUIGestureRecognizerSubclassを利用してジェスチャー認識を拡張することができます。
    手順は、以下の通りです。
    1. UIGestureRecognizerSubclass.hをインポートし、UIGestureRecognizerを継承する
      #import <UIKit/UIGestureRecognizerSubclass.h>
      
    2. 以下のメソッドをオーバーライドする
      - (void)reset;
      - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
      - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
      - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
      - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
      
    3. 上記のreset以外(touchesBegan,touchesMoved,touchesEnded,touchesCancelled)のメソッドで、現在の状態を表す「self.state」に以下の値を代入する
      typedef enum {
          UIGestureRecognizerStatePossible,
          UIGestureRecognizerStateBegan,
          UIGestureRecognizerStateChanged,
          UIGestureRecognizerStateEnded,
          UIGestureRecognizerStateCancelled,
          UIGestureRecognizerStateFailed,
          UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded
      } UIGestureRecognizerState;
      
    「1.」「2.」はそのままなのですが、「3.」は、iOS 4とiOS 5とで挙動が異なっているので注意が必要でした。後述します。

    単発のジェスチャーと連続的なジェスチャー

    まず、どのような状態があるのかですが、実装するジェスチャーが「単発」なのか「連続的」なのかで異なります。
    • 単発のジェスチャー
      • 一つの状態変化だけで表されるものです。例えば、タップやスワイプは一度タッチ動作が発生してすぐに終わりなので、単発のジェスチャーです。
    • 連続的なジェスチャー
      • 状態変化が連続的に発生するものです。例えば、ピンチイン・ピンチアウトなどで縮小・拡大が必要な場合、大きさが変わってゆくので連続的なジェスチャーとなります。
    動かすから連続的なジェスチャーであるというわけではなく、「ジェスチャーが成功したかどうかが必要」な場合には単発のジェスチャー、「ジェスチャーから連続的に発生する情報が必要」な場合には連続的なジェスチャーとして実装することになります。

    状態遷移図

    次に、状態遷移ですが、それぞれ以下の通りです。長いのでUIGestureRecognizerStateの部分を省略しています。赤い線はiOS 4とiOS 5とでの違いですが、説明はこの後にします。
    しかし実際には、連続的なジェスチャーであっても「UIGestureRecognizerStatePossible -> UIGestureRecognizerStateRecognized」という遷移を辿ることもあるので、実装する際には以下の遷移図で考える必要があります。

    ハンドラメソッドの呼び出され方

    そして、各メソッドの処理順序です。単発のジェスチャーの場合は「UIGestureRecognizerStateRecognizedかUIGestureRecognizerStateFailed」になると一度だけハンドラが実行されて終わります。
    しかし、連続的なジェスチャーの場合は少し複雑で、「UIGestureRecognizerStateBeganかUIGestureRecognizerStateChanged」に変化すると、それ以降は常にハンドラが呼ばれるようになります。「state」に値を代入したらその時だけハンドラが処理されるのかと思っていたのですが、一度でも「UIGestureRecognizerStateBeganかUIGestureRecognizerStateChanged」の状態にすると、「UIGestureRecognizerStateEndedかUIGestureRecognizerStateCancelled」を代入するまでハンドラが処理され続けます。
    なお、単発のジェスチャーでも連続的なジェスチャーでも「UIGestureRecognizerStatePossible」の状態ではハンドラは呼ばれません。
    複雑なので、何かあったら見る程度で良いと思います。

    連続的なジェスチャーが上の図だけでは分かりづらいので、iOS 4とiOS 5との違いも含めて説明します。基本的な流れは説明した通り、以下の順にメソッドが呼ばれます。
    • タッチされたらtouchesBegan
    • 動かされたら、動かされる度にtouchesMoved
    • 離されたらtouchesEnded
    iOS 4の場合は、「self.state = UIGestureRecognizerStateBegan」とすると、別な値を代入するまで「state」に変化はありません。
    しかしiOS 5の場合は、「self.state = UIGestureRecognizerStateBegan」とすると、次のメソッドが呼ばれる一度だけ「UIGestureRecognizerStateBegan」で、それ以降の呼び出しは自動的に「UIGestureRecognizerStateChanged」になってしまいます。
    確かにドキュメントを見ても、
    単発のジェスチャ用のGesture Recognizerは、PossibleからRecognized(UIGestureRecognizerStateRecognized)に遷移します。一方、連続的なジェスチャ用のGesture Recognizerは、最初にジェスチャを認識したときは、PossibleからBegan (UIGestureRecognizerStateBegan)に遷移します。次に、BeganからChanged (UIGestureRecognizerStateChanged)に遷移します。その後は、ジェスチャに変化があるたびにChangedからChangedに遷移します。
    と「UIGestureRecognizerStateBeganからUIGestureRecognizerStateChanged」に変わる条件が書かれていません。サブクラスを実装するときやハンドラの中で「state」で条件分岐する際には注意が必要です。

    サンプル

    ソースコードはGitHubに公開してあります。
    iOS-SampleCodes/GestureRecognizer-Customized - GitHub

    実用性もありそうな「マウスジェスチャー機能」をサンプルとしました。
    価格: 無料 (記事公開時)
    カテゴリ: ユーティリティ
    App Storeで詳細を見る。
    Sleipnir Mobile for iPhone / iPadでも実装されており、便利です。今回は「StrokeGestureRecognizer」クラスとして、Viewはジェスチャーの軌跡を描くだけのものです。

    マウスジェスチャー機能をカスタムGestureRecognizerとして実装する方法として色々と考えましたが、認識させたい形を複数のジェスチャーとして登録し、それぞれのジェスチャーが認識される(あるいは違うと判定される)と、ハンドラが実行されるようにしてあります。
    /* "I型" のジェスチャー */
    StrokeGestureRecognizer *iShapedGesture = [[StrokeGestureRecognizer alloc] initWithTarget:self action:@selector(handleIShapedGesture:)];
    
    // 複数のジェスチャーを認識させるためのデリゲート
    iShapedGesture.delegate = self;
    
    // ジェスチャーとして認識させるために必要な線の長さ
    [iShapedGesture setLowerLimitOfStrokeLength:45];
    
    // 下
    [iShapedGesture addWantedStroke:StrokeGestureRecognizerDirectionDown];
    
    // Viewに登録
    [self.view addGestureRecognizer:iShapedGesture];
    [iShapedGesture release];
    
    
    /* "L型" のジェスチャー */
    StrokeGestureRecognizer *lShapedGesture = [[StrokeGestureRecognizer alloc] initWithTarget:self action:@selector(handleLShapedGesture:)];
    lShapedGesture.delegate = self;
    [lShapedGesture setLowerLimitOfStrokeLength:45];
    
    // 下・右の順
    [lShapedGesture addWantedStroke:StrokeGestureRecognizerDirectionDown];
    [lShapedGesture addWantedStroke:StrokeGestureRecognizerDirectionRight];
    
    // Viewに登録
    [self.view addGestureRecognizer:lShapedGesture];
    [lShapedGesture release];
    

    setLowerLimitOfStrokeLengthメソッドでジェスチャーとして認識させるために必要な線の長さを決め、addWantedStrokeに必要なジェスチャーの順番を登録してゆきます。その後、ジェスチャーが認識されると登録したハンドラが呼ばれます。

    UIGestureRecognizerは、デフォルトでは認識させたいジェスチャーを複数登録しても、複数のジェスチャーを認識してくれません。そのため、
    UIGestureRecognizerDelegateのgestureRecognizerをオーバーライドすることにより、複数のジェスチャーを同時に認識させています。
    - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
    {
        return YES;
    }
    

    また、GestureRecognizerはデフォルトのままだと「Recognized/Failed/Ended/Cancelled」の状態になるとタッチイベントをビューに配信するのを止めてしまいます。そのため、
    self.cancelsTouchesInView = NO;
    として、タッチイベントの配信をキャンセルしないようにしています。

    スクリーンショット

    登録されていないものは認識できません

    I字(下)と、L字(下→右)の認識結果

    指を離さなくてもジェスチャーを認識し、離すと決定します

    U字、O字、C字の例

    いずれも、方向があっていれば自然な流れで認識できます

    次回

    次は、モーションイベントの検知で「モーションイベントサンプル1 - シェイクと回転検知」です。