甘いものが好きです

iOS App開発時に感じた疑問や課題、その他の雑感などを書いていきます。

Xcodeのドキュメンテーション閲覧用にDashをインストールしてみた

APIドキュメントを閲覧する上で「Dash」というアプリが便利だという情報を目にし、無料だということもあってさっそくインストールしてみた。

インストール後、初回起動時にXcodeのドキュメントを読み込まれ、すぐに準備完了。検索用のキーワードを入力中に動作が重くなることもないし、検索結果でiOS用のドキュメントとOS X用のドキュメントの違いが一目瞭然なのが良い。ドキュメントと同時にStack OverflowやGoogleも対象にして検索をかけるのも面白い。

実行時警告「Attempt to dismiss from view controller while a presentation or dismiss is in progress!」への対応

EventKitを利用したAppを開発していたとき、イベント編集画面のView Controller*1上で「完了」/「キャンセル」/「イベントを削除」のいずれかのボタンがタップされたタイミングでこのView Controllerをdismissするために、eventEditViewController:didCompleteWithAction:の中で次のようなコードを書いた。

- (void)eventEditViewController:(EKEventEditViewController *)controller didCompleteWithAction:(EKEventEditViewAction)action {
    
    switch(action) {
        case EKEventEditViewActionCanceled:
            // 必要な処理をここに追加
            break;
        case EKEventEditViewActionSaved:
            // 必要な処理をここに追加
            break;
        case EKEventEditViewActionDeleted:
            // 必要な処理をここに追加
            break;
        default:
            break;
    }
    [self dismissViewControllerAnimated:YES completion:NULL];
}

このコードを実行したところ、正常にdismissすることができたのだが、実行時にデバッグコンソールに以下の警告が出力された。

Warning: Attempt to dismiss from view controller while a presentation or dismiss is in progress!

対処方法を調べたところ、このページが参考になった。

この警告はEKEventEditViewControllerに限った話ではなく、他のView Controllerを利用する場合でもdismiss時に起こることがあるようだ。

警告の文言どおり、View Controllerがdismissされている途中ではない場合にのみ、View Controllerをdismissすればよい。

    if (![[self controller] isBeingDismissed]) {
        [self dismissViewControllerAnimated:YES completion:NULL];
    }

*1:EKEventEditViewController。事前にpresentViewController:animated:completion:によって表示しておいたもの。

NSScannerを利用して16進数の文字列を10進数の数値に変換

16進数の文字列から10進数の数値を得る方法を探していたら、NSScannerを使うと便利だという情報を発見。

NSString *colorCode = @"0x80ffc4";

unsigned int rgb[3];
for (int i = 0; i < 3; i++) {
    NSString *component = [colorCode substringWithRange:NSMakeRange(i * 2, 2)];
    NSScanner *scanner = [NSScanner scannerWithString:component];
    [scanner scanHexInt:&rgb[i]];
}

NSScannerの使い方は『詳解iOS5プログラミング』の2.3.1節「文字列と数値の変換」で触れられており、そこではscanUpToString:intoString:によるスキャン位置の移動についても説明されている。

詳解iOS5プログラミング

詳解iOS5プログラミング

ちなみに、数値から文字列への変換は、NSStringクラスのstringWithFormat:で可能だ。

UIImagePickerControllerでデバイスの向きの変更を無視する

UIImagePickerControllerを利用して写真を撮影するとき、Portrait固定にしたいのになかなかできず苦労した。

失敗例

以下の方法を試してみたが、うまくいかなかった。

  • Xcodeのビルドターゲットの設定で「Supported Interface Orientations」を「Portrait」のみを選択する。
  • UINavigationControllerのカテゴリを作成し、そこで次のメソッドを実装し、回転の無効化(Portrait固定設定)を試みる*1
    • supportedInterfaceOrientations
    • shouldAutorotate
    • shouldAutorotateToInterfaceOrientation(iOS5以下)
  • UIImagePickerControllerのサブクラスを作成し、カメラ撮影にはこのクラスを利用する。このサブクラスでは次のメソッドを実装し、回転の無効化(Portrait固定設定)を試みる。
    • supportedInterfaceOrientations
    • shouldAutorotate
    • shouldAutorotateToInterfaceOrientation(iOS5以下)

解決策

次のページの回答にある方法で無事、デバイスの回転を無視できるようになった。

回答のソースコードではtry-catchを使っているが、カメラが利用できるかどうかの判定はUIImagePickerControllerのisSourceTypeAvailable:でできるので、少しだけ修正すると次のように書ける。

if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {

    // Create a UIImagePicker in camera mode.
    UIImagePickerController *picker = [[[UIImagePickerController alloc] init] autorelease]; 
    picker.sourceType = UIImagePickerControllerSourceTypeCamera;  
    picker.delegate = self; 

    // Prevent the image picker learning about orientation changes by preventing the device from reporting them.
    UIDevice *currentDevice = [UIDevice currentDevice];

    // The device keeps count of orientation requests.  If the count is more than one, it continues detecting them and sending notifications.  So, switch them off repeatedly until the count reaches zero and they are genuinely off.
    // If orientation notifications are on when the view is presented, it may slide on in landscape mode even if the app is entirely portrait.
    // If other parts of the app require orientation notifications, the number "end" messages sent should be counted.  An equal number of "begin" messages should be sent after the image picker ends.
    while ([currentDevice isGeneratingDeviceOrientationNotifications])
        [currentDevice endGeneratingDeviceOrientationNotifications];

    // Display the camera.
    [self presentModalViewController:picker animated:YES];

    // The UIImagePickerController switches on notifications AGAIN when it is presented, so switch them off again.
    while ([currentDevice isGeneratingDeviceOrientationNotifications])
        [currentDevice endGeneratingDeviceOrientationNotifications];
}

あと、この回答では触れられていないけれど、UIImgaePickerControllerをdismissしてからViewの回転を明示的に許可する必要がある。

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
    
    [picker dismissViewControllerAnimated:YES completion:^{
        UIDevice *currentDevice = [UIDevice currentDevice];
        while (![currentDevice isGeneratingDeviceOrientationNotifications])
            [currentDevice beginGeneratingDeviceOrientationNotifications];
    }];
}

- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
    
    [picker dismissViewControllerAnimated:YES completion:^{
        UIDevice *currentDevice = [UIDevice currentDevice];
        while (![currentDevice isGeneratingDeviceOrientationNotifications])
            [currentDevice beginGeneratingDeviceOrientationNotifications];
    }];
}

isGeneratingDeviceOrientationNotifications、beginGeneratingDeviceOrientationNotifications、endGeneratingDeviceOrientationNotificationsは今まで使ったことがなかった(存在自体に気がついていなかった)のだけれど、一時的にViewの回転を無効化するときに役立ちそうだ。

*1:カテゴリで既存メソッドの上書きをすると、新たに定義された方のメソッドが有効になる。『詳解 Objective-C 2.0 第3版』10-02節の「既存のメソッドの上書き」を参照。

XcodeでLeap Motion対応アプリ開発の準備(Objective-C版)

Leap Motion対応アプリはさまざまなプログラミング言語で開発することができるが、今回は、Xcode上で主にObjective-Cを使ってMac Appを開発することにした。

開発環境は次のとおり。

  • OS X 10.8.2 (12C60)
  • Xcode 4.6 (4H127)
  • Leap Developer Kit 0.7.3

Leap Motion Developer Potalで配布されているLeap SDKの中には、サンプルのXcodeプロジェクトがある。Xcodeの新規プロジェクトを作成し、このサンプルプロジェクトと同じものを一から作ってみた。その手順と、その中で苦労した点についてまとめておく。

Leap関連のヘッダ/ソースファイルをXcodeプロジェクトに追加

Xcodeプロジェクトに次のファイルを追加する。

  • Leap.h
  • LeapMath.h
  • LeapObjectiveC.h
  • LeapObjectiveC.mm

ファイル追加時に表示される「Choose options for adding these files」画面では、アプリのビルドに使用するターゲットについて「Add to targets」でチェックを入れる。

正しく追加されると、「Compile Sources」*1に「LeapObjectiveC.mm」が含まれている。

ヘッダ検索パス(必要ならば)

上記ファイルの置き場によっては、ヘッダ検索パスの設定*2を変更しておかなければならない。

C++ Standard Library

また、Xcodeプロジェクトのデフォルトでは「C++ Standard Library」の設定*3で「libc++ (LLVM C++ standard library with C++11 support)」が選択されているので、これを「libstdc++ (GNU C++ standard library)」に変更する。

この設定変更を行わなければ、次のようなビルドエラーが発生する。

Undefined symbols for architecture x86_64:
"Leap::Config::type(std::__1::basic_string, std::__1::allocator > const&) const", referenced from:
-[Config type:] in LeapObjectiveC.o
...
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

libLeap.dylibをXcodeプロジェクトに追加

libLeap.dylibをXcodeプロジェクトに追加する。Leap SDKにはlibLeap.dylibという名前のファイルが2つあるので、「libc++」フォルダ配下にあるものではなく、もう一方のlibLeap.dylibを使用するように注意する。

ライブラリファイル追加時に表示される「Choose options for adding these files」画面では、アプリのビルドに使用するターゲットについて「Add to targets」でチェックを入れる。

正しく追加されると、「Link Binary With Libraries」と「Copy Bundle Resources」の両方*4に「libLeap.dylib」が含まれている。

ライブラリ検索パス(必要ならば)

ライブラリの置き場によっては、ライブラリ検索パスの設定*5を変更しておかなければならない。

Run Scriptの設定

アプリ実行時に動的にライブラリファイルの位置指定を変更する。次のようなRun Scriptを設定すればよい。

# print some debug information
echo TARGET_BUILD_DIR=${TARGET_BUILD_DIR}
echo TARGET_NAME=${TARGET_NAME}
cd ${TARGET_BUILD_DIR}/${TARGET_NAME}.app/Contents/MacOS
ls -la
# then remap the loader path
install_name_tool -change @loader_path/libLeap.dylib @executable_path/../Resources/libLeap.dylib ${TARGET_NAME}

この設定を行わなければ、次のような実行時エラーが発生する。

dyld: Library not loaded: @loader_path/libLeap.dylib
Referenced from: ...
Reason: image not found

これで準備OK

これで準備が終わったので、この後は実際にLeap Motionを使って各種データを取得するコードを書いていく。

*1:Project Navigator上でプロジェクトを選択 -> アプリのビルドに使用するターゲットを選択 -> 「Build Phases」タブを選択

*2:Project Navigator上でプロジェクトを選択 -> アプリのビルドに使用するターゲットを選択 -> 「Build Settings」タブを選択 -> 「Search Paths」セクション内の「Header Search Paths」

*3:Project Navigator上でプロジェクトを選択 -> アプリのビルドに使用するターゲットを選択 -> 「Build Settings」タブを選択 -> 「Apple LLVM compiler 4.2 - Language」セクション内の「C++ Standard Library」

*4:Project Navigator上でプロジェクトを選択 -> アプリのビルドに使用するターゲットを選択 -> 「Build Phases」タブを選択

*5:Project Navigator上でプロジェクトを選択 -> アプリのビルドに使用するターゲットを選択 -> 「Build Settings」タブを選択 -> 「Search Paths」セクション内の「Library Search Paths」

Xcode 4.6から利用可能になったビルド設定「Empty Loop Bodies」を試してみた

以下の記事によると、昨日リリースされたXcode 4.6では新たなビルド設定「Empty Loop Bodies」が追加されたとのこと。

この設定を有効にすると、if文やwhile文などで条件を記述した括弧の直後にセミコロンを打っている場合には、

Semantic Issue
If statement has empty body

あるいは

Semantic Issue
While loop has empty body

という警告が出るようになる。

どのような書き方をすると警告が出るようになるのか、具体的に調べてみた。*1

デフォルトでは有効

Xcode 4.5.2で作成したプロジェクトをXcode 4.6で開いてプロジェクト設定を確認してみたところ、「Empty Loop Bodies」には「YES」が設定されていた。どうやらデフォルトで有効になっているようだ。

for/while文

括弧の直後にセミコロンを打ち、同じ行に他の文を続けると警告が出る。

int i = 0;
while (i < 10); i++; // この行に対して警告が出る

改行する場合には、次行をインデントすると警告が出るが、

int i = 0;
while (i < 10); // この行に対して警告が出る
    i++;

次行をインデントしないと(または次行が空行だと)警告は出ない。

// この書き方だと警告は出ない
int i = 0;
while (i < 10);
i++;

if文

if文はfor/while文よりもチェックが厳しい。括弧の直後にセミコロンを打つだけで必ず警告が出る。

int i = 0;
if (i < 10); // この行に対して警告が出る

*1:Xcode 4.6 (4H127)で確認。

MRCとARCの両方に対応したコードをシンプルに書く方法

MRC(Manual Reference Counting)とARC(Automatic Reference Counting)の両方に対応したコードを書くためには

#if __has_feature(objc_arc)
#if __has_feature(objc_arc_weak)

という分岐により条件つきコンパイルをしなければならない。
今となっては少々古い話題ではあるが*1、気になったのでなるべく簡単に対応する方法を調べてみた。異なるビルド環境にソースコード側で対応する場合このような方法があること自体、覚えておいて損はないだろうと思う。

MRCとARCの両方に対応する上で気をつけなければならない点は主に次の2点だ。

  1. 使用可能なライフタイム修飾子の違い
  2. 明示的なretain/releaseのメッセージ送信の必要有無

環境に応じてライフタイム修飾子を適切に使い分ける

ブロックオブジェクトの中で変数にアクセスする場合、解放済オブジェクトにアクセスしたり循環参照が生じてしまうおそれがある。そこで、各変数に適切なライフタイム修飾子を設定しておく必要がある。ここで問題となるのは、ビルド時に使用するSDKやDeployment Targetとして指定するiOSのバージョンによって、使用可能なライフタイム修飾子が異なるという点である。

次のページでは、MRCとARCの双方の環境においてブロックオブジェクトを安全かつ容易に扱うための方法が説明されている。

このページの中で、ライフタイム修飾子についてのマクロを定義する方法が示されている。iOS4.3とiOS5以降の違いは今もなお意識すべきポイントであるため、大変参考になる。該当箇所のみを以下に抜粋する。

// ARC & memory management
// Use these prefixes to be compatible with ARC on iOS 5/ ARC on iOS 4.X / non-ARC
// 
#if __has_feature(objc_arc_weak) // iOS 5 or above
#define __my_block_weak        __weak
#define __my_block_weak_unsafe __weak
#elif __has_feature(objc_arc)    // iOS 4.X
#define __my_block_weak        __strong
#define __my_block_weak_unsafe __unsafe_unretained
#else                            // iOS 3.X or non-ARC projects
#define __my_block_weak        __strong
#define __my_block_weak_unsafe __block
#endif

retain/releaseメッセージもマクロ化

retain、release、autoreleaseなどの保持・解放に関するメッセージを送信する部分のコードも、できることなら毎回__has_feature(objc_arc)で分岐させずにシンプルに書きたい。この点については次のページでマクロの定義の例が掲載されている。

マクロ部分のみを抜粋すると以下のとおり。

#ifndef AH_RETAIN
#if __has_feature(objc_arc)
#define AH_RETAIN(x) x
#define AH_RELEASE(x)
#define AH_AUTORELEASE(x) x
#define AH_SUPER_DEALLOC
#else
#define __AH_WEAK
#define AH_WEAK assign
#define AH_RETAIN(x) [x retain]
#define AH_RELEASE(x) [x release]
#define AH_AUTORELEASE(x) [x autorelease]
#define AH_SUPER_DEALLOC [super dealloc]
#endif
#endif

*1:Xcode 4.5で指定できるDeployment TargetはiOS4.3かそれ以降に制限されている。