ラベル iPhone の投稿を表示しています。 すべての投稿を表示
ラベル iPhone の投稿を表示しています。 すべての投稿を表示

[iOS] バックグラウンド実行見本(Task Completion)

2011年4月8日金曜日 | Published in | 2 コメント

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

(2011-12-01 追記あり)UIApplicationDelegateの呼び出しが iOS5 から変わった件。

Task Completion を使った iOS4 でのバックグラウンド実行サンプルを作ってみた。

サンプル


実行するとキューにたまった 30個のデータが順番に処理されてテーブルから消えていく。

処理は GCD を使い別スレッドで実行される。右上の[+]ボタンを押すとキューへデータが追加されていく。途中でホームボタンを押してアプリを切り替えても Task Completion によって処理は停止すること無く実行され続ける。わかりやすいようにアプリのアイコンバッヂに残タスク数を表示してみた。
バッジの数字は時間と共にカウントダウンされていくので処理が行われていることが確認できる。


Task Completion とは?


Task Completion は、iOS4 から導入されたマルチタスキングの機能の一つで、これを利用すると最大10分間を上限にバックグラウンドで処理を実行できる。利用するには -[UIApplication beginBackgroundTaskWithExpirationHandler:] を使う。このメソッドは iOS に対してバックグラウンド時の処理続行を依頼するものなので通常はアプリケーションがバックグラウンド状態になるタイミング(applicationDidEnterBackground:)などで呼び出す。公式リファレンスでは次のようなコードが紹介されている。
iOS Application Programming Guide: Executing Code in the Background より

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    UIApplication*    app = [UIApplication sharedApplication];
 
    bgTask = [app beginBackgroundTaskWithExpirationHandler:^{
        [app endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];
 
    // Start the long-running task and return immediately.
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
 
        // Do the work associated with the task.
 
        [app endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    });
}
beginBackgroundTaskWithExiprationHandler: を呼び出すと、それ以降別のアプリを使用している間も処理を続行させることができる。このメソッドの引数 blocks には、処理が10分過ぎても終わらない時に実行する処理を書いておく。endBackgroundTask: はバックグラウンド処理が終わった時に呼び出す。この引数は beginBackgroundTaskWithExiprationHandler: の戻り値を渡す。


ソースコード解説


サンプルでの Task Completion の利用方法を見ていく。今回は簡易的なキュー(FIFOバッファ)を用意して、そこに入っているデータを単純に取り出すだけの処理を行うスレッドを実行させた。キューの定義はこんな感じ。
@interface Queue : NSObject {

}
@property (retain) NSMutableArray* queue;

- (void)putObject:(id)object;
- (id)getObject;
- (void)removeObject;
- (NSUInteger)count;
- (NSArray*)list;
@end
次にアプリケーション初期化処理。
- (BOOL)application:(UIApplication *)application
 didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // [1] creating sample data and put into the queue
    Queue* queue = [[Queue alloc] init];
    for (int i=0; i < 60; i++) {
        NSString* str = [NSString stringWithFormat:@"DATA-%02d", i];
        [queue putObject:str];
    }
    self.rootViewController.queue = queue;
    [queue release];

    // [2] init window    
    self.window.rootViewController = self.navigationController;
    [self.window makeKeyAndVisible];
    
    // [3] start thread
    dispatch_queue_t gcd_queue =
       dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(gcd_queue, ^{
        
        UIApplication* app = [UIApplication sharedApplication];
        app.applicationIconBadgeNumber = [queue count];
        for(;;) {
            if ([queue count] > 0) {
                id object = [queue getObject];
                NSLog(@"processing: %@", object);
                [NSThread sleepForTimeInterval:1.0];    // dummy wait
                NSLog(@"done: %@", object);
                [queue removeObject];
                app.applicationIconBadgeNumber = [queue count];
                
                dispatch_async(dispatch_get_main_queue(), ^{
                    [self.rootViewController.tableView reloadData];
                });
                
                if ([queue count] == 0 && backgroundTaskIdentifer != UIBackgroundTaskInvalid) {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        NSLog(@"finished!");
                        if (backgroundTaskIdentifer != UIBackgroundTaskInvalid) {
                            [app endBackgroundTask:backgroundTaskIdentifer];
                            backgroundTaskIdentifer = UIBackgroundTaskInvalid;
                        }
                    });
                }
            } else {
                [NSThread sleepForTimeInterval:1.0];
            }
        }
        
    });
    
    return YES;
}
前半ではキューへデータを入れたり[1]、ウィンドウの初期化[2]を行っている。後半の [3]で GCDを使いスレッドを一つ作成し、そこで定期的にキュー内のデータを処理させている。このスレッド内ではキューから1づつデータを取り出し処理(単純に1秒スリープ)を繰り返し行う。すべての処理が終わったときに Task Completion が有効( != UIBackgroundTaskInvalid)なら endBackgroundTask: を読んでバックグラウンド処理を終了させている。キューが空になった時には1秒間スリープし、その後キューにデータがあれば処理を実行し、無ければ再びスリープする動作を繰り返す。

Task Completion を有効にする処理は applicationWillResignActive: に書いている。
- (void)applicationWillResignActive:(UIApplication *)application
{
    NSLog(@"%s", __PRETTY_FUNCTION__);
    UIApplication* app = [UIApplication sharedApplication];
    
    NSAssert(backgroundTaskIdentifer == UIBackgroundTaskInvalid, nil);
    
    backgroundTaskIdentifer = [app beginBackgroundTaskWithExpirationHandler:^{
        
        NSLog(@"expired!");
        dispatch_async(dispatch_get_main_queue(), ^{
            if (backgroundTaskIdentifer != UIBackgroundTaskInvalid) {
                [app endBackgroundTask:backgroundTaskIdentifer];
                backgroundTaskIdentifer = UIBackgroundTaskInvalid;
            }
        });
    }];
    
}
なお Task Completion を有効にした後に別アプリへ切り替え、さらにその後再びこのアプリへ戻ってきた時にまだ Task Completion が有効だった場合には endBackgroundTask: を呼び出してやる必要がある。これを applicationDidBecomeActive: に書いておく。
- (void)applicationDidBecomeActive:(UIApplication *)application
{
    NSLog(@"%s", __PRETTY_FUNCTION__);
    
    UIApplication* app = [UIApplication sharedApplication];
    dispatch_async(dispatch_get_main_queue(), ^{
        if (backgroundTaskIdentifer != UIBackgroundTaskInvalid) {
            [app endBackgroundTask:backgroundTaskIdentifer];
            backgroundTaskIdentifer = UIBackgroundTaskInvalid;
        }
    });
}

Task Completion の制御を
applicationDidEnterBackground:
applicationWillEnterForeground:
の組ではなく
applicationWillResignActive:
applicationDidBecomeActive:
の組を使うのは、スリープボタンを押した時は前者の組み合わせは呼ばれない為。後者であればスリープ => 復帰の時に呼び出されるので今回の目的に適している。

(2011-12-01 追記)iOS5からスリープボタンを押した時でも前者の組み合わせが呼び出されるとのこと。(@hkato193 さん Thanksです)

参考:Cocoaの日々: UIApplicationDelegate のマルチタスキング関連メソッド調査

ちなみに Task Completion を使うとスリープ中もバックグラウンド処理は走り続ける(上限10分)。


ダウンロード


サンプルのソースは GitHub からどうぞ。

BackgroundQueueSample/BackgroundQueueSample at 2011-04-08b from xcatsan/iOS-Sample-Code - GitHub


参考情報



Task Completion の情報は @hkato193さんが書いている第2章の「マルチタスキング」が詳しい。入手可能な日本語の情報ではこれが一番いい。
同様に GCD や Blocks の解説もこの本の第5章「マルチスレッド」(@splhackさん執筆)が詳しくておすすめ。

iOS Application Programming Guide: Executing Code in the Background
Apple提供情報。

NSCalendar - +currentCalendar より -initWithCalendarIdentifier: を使う

2010年12月17日金曜日 | Published in | 0 コメント

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

覚書。

+[NSCalendar currentCalendar] で取得する NSCalendar のインスタンスは「言語環境 - カレンダー」設定によって決められる。
「和暦」を選んだ場合、-components:fromDate: で取得できる年の値は(当たり前だが)平成の年数となる(例:22)。

+currentCalendar から取得できる NSCalendarのインスタンスは利用環境に依存する挙動となるため、年数計算等で NSCalendar を使用する場合は +currentCalendar を使わず統一したカレンダを利用する。
(例) NSCalendar *calendar = [[NSCalendar alloc]
        initWithCalendarIdentifier:NSGregorianCalendar];
通常は NSGregorianCalendar で良いと思う。


NSCalendar Class Reference

ネットワーク接続状況を知る[2] SCNetworkReachabilityGetFlags はブロックする

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

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

[2011-07-08追記] ブロックの原因が判明、下記もどうぞ。
Cocoaの日々: [iOS] SCNetworkReachabilityGetFlags のブロックの件


以前、Cocoaの日々: ネットワーク接続状況を知る というブログを書いた。

その後わかったことを紹介する。

SCNetworkReachabilityGetFlags がブロックする件


ネットワーク接続状況を取得する SCNetworkReachabilityGetFlags関数 が処理中にブロックしてしまうことがわかった。次の環境でブロックする現象が確認できた。

・iPhone 3GS/iOS4.2
・3GS→× WiFi→◯ ※フライトモード+WiFi有効
・WiFiルータ→×   ※インターネット非接続(光回線断)

つまり iPhone で WiFi接続しているが、その接続先の WiFiルータから先のインターネットに接続していない。この環境で関数を呼ぶとそこで処理が停止し、60秒後にアプリケーションの動作が再開する。60秒を待たずにアプリケーションが落ちたケースもあった(クラッシュログから SCNetworkReachabilityGetFlags が原因であることが分かっている)。

タイムアウトは実測値で60秒。変更できそうな関数は見当たらない。

このことからSCNetworkReachability系の API を利用する場合は、SCNetworkReachabilityGetFlags は使わず、コールバック関数を使った非同期実装が良いと思われる。


サンプルプログラム NetworkRechable の改良


今回の情報を元に以前作成したサンプルプログラムを改良した。SCNetworkReachabilityGetFlags の使用をやめて SCNetworkReachabilitySetCallback を使った非同期方式に全面的に書き換えた。

インターフェイスはこう。
#import <Foundation/Foundation.h>
#import <SystemConfiguration/SystemConfiguration.h>

typedef enum {
 kNetworkReachableUninitialization = 0,
 kNetworkReachableNon,
 kNetworkReachableWiFi,
 kNetworkReachableWWAN
} NetworkReachabilityConnectionMode;

#define NetworkReachabilityChangedNotification @"NetworkReachabilityChangedNotification"

@interface NetworkReachability : NSObject {

 SCNetworkReachabilityRef reachability_;
 NetworkReachabilityConnectionMode connectionMode_;
}

+ (NetworkReachability*)networkReachabilityWithHostname:(NSString *)hostname;
- (NetworkReachabilityConnectionMode)connectionMode;
- (NSString*)connectionModeString;

@end
実装はポイントだけ。
// 初期化
- (id)initWithHostname:(NSString*)hostname
{
 if (self = [super init]) {
  reachability_=
  SCNetworkReachabilityCreateWithName(kCFAllocatorDefault,
           [hostname UTF8String]);
  connectionMode_ = kNetworkReachableUninitialization;  
  [self startNotifier_];
 }
 return self;
}

// コールバック設定
- (BOOL)startNotifier_
{
 BOOL ret = NO;
 SCNetworkReachabilityContext context = {0, self, NULL, NULL, NULL};
 if(SCNetworkReachabilitySetCallback(reachability_, ReachabilityCallback_, &context))
 {
  if(SCNetworkReachabilityScheduleWithRunLoop(
             reachability_, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode))
  {
   ret = YES;
  }
 }
 return ret;
} 

// コールバック先
static void ReachabilityCallback_(SCNetworkReachabilityRef target,
 SCNetworkReachabilityFlags flags, void* info)
{
 NSAutoreleasePool* myPool = [[NSAutoreleasePool alloc] init];
 
 NetworkReachability* noteObject = (NetworkReachability*)info;
 [noteObject updateConnectionModeWithFlags_:flags];
 
 [[NSNotificationCenter defaultCenter]
  postNotificationName:NetworkReachabilityChangedNotification object:noteObject];
 
 [myPool release];
}
内部的に SCNetworkReachabilitySetCallback を使い、コールバックされたタイミングで connectionMode_ を更新する。コールバックは接続状況に変化があったときに呼び出される(例えば、3G→WiFiや、WiFi→OFF)。


利用イメージ


NetworkReachability* networkReachability =
  [NetworkReachability networkReachabilityWithHostname:@"www.google.com"];
NetworkReachabilityConnectionMode mode = [networkReachability connectionMode];
何度も使う場合はインスタンスをとっておいて使い回せば良い。

接続状況が変化した時に通知を受け取りたい場合は NetworkReachabilityChangedNotification を監視すれば良い。
[[NSNotificationCenter defaultCenter] addObserver:self
            selector:@selector(reachabilityChanged:)
             name:NetworkReachabilityChangedNotification
              object: nil];
- (void) reachabilityChanged: (NSNotification* )note
{
 NSLog(@"changed:%@", note);
 [self updateStatus];
}


備考


初回のタイムラグ

コールバックを使った非同期式の場合、接続状況が変化した時の通知に若干のタイムラグがある(長くはない〜1秒未満)。多くの場合問題ないがアプリケーション起動時の最初の処理でこの接続状況を使う場合は問題になることがある。実際簡単なアプリケーションでは最初の画面が表示された後に最初の変更通知が届くので、最初の画面でネットワークの接続状況を確認してからアクションを起こすという処理の場合このタイムラグが問題になる。非同期処理だから本質的な解決方法は無いので、NetworkReachabilityChangedNotification の到着を待ってから処理を行うか、初回のみ接続状況は無視するような対処が必要になる。先のサンプルではこの接続状況が決まらない状態(すなわち最初の通知が来るでの状態)として kNetworkReachableUninitialization という初期値を設定している。利用側は -[NetworkReachability connectionMode] でこの値が帰ってきた場合、このタイムラグ中であることが判断できるので、それを元になんらかの対処を行う方法が考えられる。

SCNetworkReachabilityCreateWithName()で指定するホスト

SCNetworkReachability インスタンスを作成する時に使う SCNetworkReachabilityCreateWithName 関数へ渡すホスト名は現実に存在してネットワークで到達可能なものを指定する必要がある。試しに dummy.dummy.dummy のような適当なホスト名を渡したところ、常に接続不能状態となってしまった(当たり前といえば当たり前だが...)。



ソースコード


GitHubからどうぞ。
NetworkReachable at 2010-12-13 from xcatsan/iOS-Sample-Code - GitHub



参考情報


SCNetworkReachability Reference

[iOS] GameKit - WiFi接続

2010年12月11日土曜日 | Published in | 2 コメント

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

[前回] Cocoaの日々: [iOS] GameKit - Bluetoothを使った iOSデバイス間の通信 [2]画像の送受信

Bluetooth に代えてWiFiを使ったピア接続をやってみる。

GKPeerPickerConnectionTypeOnline


WiFi経由で接続する場合は GKPeerPickerController.connectionTypesMask に GKPeerPickerConnectionTypeOnline を加える。
picker.connectionTypesMask = GKPeerPickerConnectionTypeOnline|GKPeerPickerConnectionTypeNearby;
GKPeerPickerConnectionTypeOnline だけでは駄目で GKPeerPickerConnectionTypeNearby を一緒に指定する必要がある。設定しない場合エラーとなる。
[2152:307] *** Terminating app due to uncaught exception 'GKInvalidArgumentException',
 reason: 'A GKPeerPickerController must support GKPeerPickerConnectionTypeNearby at a minimum.'

すると最初に WiFiと Bluetoothのどちらを使うか選択ダイアログが表示されるようになる。


GKSession作成


WiFi の場合は GKPeerPickerControllerDelegate の -[GKPeerPickerController peerPickerController:didSelectConnectionType:] を実装する。
- (void)peerPickerController:(GKPeerPickerController *)picker
 didSelectConnectionType:(GKPeerPickerConnectionType)type
{
 if (type == GKPeerPickerConnectionTypeOnline) {
  picker.delegate = nil;
  [picker dismiss];
  [picker autorelease];
  
  self.session = [[[GKSession alloc] initWithSessionID:nil
             displayName:nil
             sessionMode:GKSessionModePeer] autorelease];
  self.session.delegate = self;
  self.session.available = YES;
  [self.session setDataReceiveHandler:self withContext:nil];
 }
}
WiFi の場合 GKPeerPickerController がやってくれるのはここまでで、これ以降は接続まで自前で実装する必要がある(Bluetoothの場合は接続可能な端末がリストアップされ選択=接続することができる)。

GKSession を作成した後、-[GKSessionDelegate session:peer:didChangeState:] が呼び出されて GKSessionが利用可能になる(GKPeerStateAvailable)。その時点で接続可能な端末が見つかると割り当てられた peerID から端末名を取得することができる。これをリストアップすれば Bluetoothの場合と同様に接続可能リストが表示できそうだ。今回は無条件に connectToPeer:withTimeout: を呼び出して見つかった端末へ接続しに行く。
- (void)session:(GKSession *)session peer:(NSString *)peerID
 didChangeState:(GKPeerConnectionState)state
{
 switch (state) {
  case GKPeerStateAvailable:
   NSLog(@"connecting to %@ ...", [session displayNameForPeer:peerID]);
   [session connectToPeer:peerID withTimeout:10];
   break;
   
  case GKPeerStateConnected:
   self.message.text = @"connected";
   self.peerID = peerID;
   break;
  case GKPeerStateDisconnected:
   self.message.text = @"disconnected";
   self.session = nil;   
  default:
   break;
 }
}
このプログラムを2台の端末で実行すると両方で接続要求をだすので片方はエラーが出る。
Error Domain=com.apple.gamekit.GKSessionErrorDomain Code=30510
 "Connection to peer already in progress after initially succeeding." UserInfo=0x9342b10 
{NSLocalizedFailureReason=Found in progress after success., NSLocalizedDescription=Connection
 to peer already in progress after initially succeeding.}
しかし、その後片方が成功して接続が確立する。後は Bluetooth同様にデータの送受信が可能。


ソースコード


BluetoothSample at 2010-12-11 from xcatsan/iOS-Sample-Code - GitHub


参考情報


Game Kit Programming Guide: About Game Kit

iOS Reference Library
Game Kit Programming Guide の日本語版(PDF)が提供されている。

[iOS] GameKit - Bluetoothを使った iOSデバイス間の通信 [2]画像の送受信

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

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

[前回] Cocoaの日々: [iOS] GameKit - Bluetoothを使った iOSデバイス間の通信

今回は画像を送信してみた。

サンプル


画像が選ぶと...

こうなる。転送は2秒程度。


解説


まず送信部分。UIImagePickerController の扱いは割愛する。UIImage を送る部分だけピックアップした。
- (void)imagePickerController:(UIImagePickerController *)picker
 didFinishPickingMediaWithInfo:(NSDictionary *)info
{
    [picker dismissModalViewControllerAnimated:YES];
 
 UIImage* image = [info objectForKey:UIImagePickerControllerEditedImage];
 
 NSError* error = nil;
 NSData* data = UIImageJPEGRepresentation(image, 0.5);
 [self.session sendData:data
       toPeers:[NSArray arrayWithObject:self.peerID]
     withDataMode:GKSendDataReliable
      error:&error];
 if (error) {
  NSLog(@"%@", error);
 }
 
}
特別な工夫はなく UIImage を NSData に変換して送信しているだけ。

次に受信部分。
- (void) receiveData:(NSData *)data fromPeer:(NSString *)peer
 inSession:(GKSession *)session context:(void *)context
{
  // image
  NSLog(@"received image");
  self.imageView.image = [UIImage imageWithData:data];
 }
}
これも工夫は無い。送られてきた NSData から UIImageを生成して表示しているだけ。

なお大きなサイズだとエラーが出ていた。
Error Domain=com.apple.gamekit.GKSessionErrorDomain Code=30202 "Send data error."
 UserInfo=0x570c1b0 {NSLocalizedFailureReason=AGPSessionSendTo failed (801C0001).,
 NSLocalizedDescription=Send data error.}
メモリが不足していたのでそれに関連してエラーが出たのかもしれない。


ソースコード


GitHub からどうぞ。
xcatsan/iOS-Sample-Code at 2010-12-10b - GitHub

[iOS] GameKit - Bluetoothを使った iOSデバイス間の通信

2010年12月9日木曜日 | Published in | 0 コメント

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

GameKit を使い端末間の Bluetooth通信を試してみた。

サンプル


テキストメッセージをやりとりできる簡易なサンプルプログラムを組んでみた。
2つの iOS端末でプログラムを立ち上げて両方で "Connect" を押す。すると近くの端末を探し始める。

Bluetooth がオフに鳴っている場合はオンにするか尋ねられる。
通信可能な端末が見つかるとリストアップされる。
選択すると相手側で受け入れの確認がある。
受け入れる(Accept)と接続が成立する。
 テキストを入れて "send" ボタンを押すと相手側の端末にそのテキストが表示される。


解説


Bluetooth を使った端末間通信を行うのに今回は GameKit.framework を使った。最初にこのフレームワークをプロジェクトへ加えておく。

実装のポイントは2つ。
1. GKPeerPickerController を使い接続を確立する
2. 接続後は GKSession を使いデータの送受信を行う

まず 1. の接続確立から。
- (IBAction)connect:(id)sender
{
 GKPeerPickerController* picker = [[GKPeerPickerController alloc] init];
 picker.delegate = self;
 picker.connectionTypesMask = GKPeerPickerConnectionTypeNearby;
 [picker show];
}
インスタンスを作り、-[show] を投げると接続に必要なダイアログが表示される。
この後は接続の確立もしくはキャンセルのタイミングで GKPeerPickerControllerDelegate のメソッドが呼ばれる。
接続が確立すると -[peerPickerController:didConnectPeer:] が呼ばれる。接続してしまえば GKPeerPickerController は不要になるので閉じておく。接続後に渡される GKSession を取っておく。この GKSession をこの後のデータ送受信で使う。
- (void)peerPickerController:(GKPeerPickerController *)picker
 didConnectPeer:(NSString *)peerID toSession:(GKSession *)session
{
 self.peerID = peerID;
 self.session = session;
 session.delegate = self;
 [session setDataReceiveHandler:self withContext:nil];
 picker.delegate = nil;
 [picker dismiss];
 [picker autorelease];
}

続いて 2. のデータ送受信を行う。まず送る側から。UITextField 内の文字列を UTF8エンコードして NSDateに変換し、これを送信する。
- (IBAction)sendText:(id)sender
{
 NSData* data = [self.sendText.text dataUsingEncoding:NSUTF8StringEncoding];

 NSError* error = nil;
 [self.session sendData:data
       toPeers:[NSArray arrayWithObject:self.peerID]
     withDataMode:GKSendDataReliable
      error:&error];
 if (error) {
  NSLog(@"%@", error);
 }
 self.sendText.text = @"";
}
次に受け側。receiveData:fromPeer:inSession:context: を実装する。ここでは渡ってきた NSData を UTF8でデコードして NSStringに戻し、画面上の UITextView へ表示している。
- (void) receiveData:(NSData *)data fromPeer:(NSString *)peer
 inSession: (GKSession *)session context:(void *)context
 NSString* msg = [[[NSString alloc] initWithData:data
   encoding:NSUTF8StringEncoding] autorelease];
 NSString* text = [self.textView.text stringByAppendingFormat:@"%@\n", msg];
 self.textView.text = text;
}

最低限のコードはこれだけ。実に簡単に Bluetooth 通信が実現できる。


ソースコード


GitHub からどうぞ。
BluetoothSample at 2010-12-09 from xcatsan/iOS-Sample-Code - GitHub


備考


Mac Book Pro 上で動かしている iPhoneシミュレータでは Bluetooth の接続確認は行えなかった。Bluetooth自体は有効になるのだが iPhoneを見つけてくれない。シミュレータでテストできるとかなり楽なのだが。今は iPhone 3GS, iPad 両方にいちいちインストールしてテストしている。WiFi接続に切り替えられるようにするとシミュレータでも動くかもしれないので後で試してみよう。

[Mac][iOS] Static Library (8) フレームワーク検索パス

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

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

[前回] Cocoaの日々: [Mac][iOS] Static Library (7) カテゴリを使う場合の注意点 "-ObjC" と "-all_load"

利用しているフレームワークのパスなどを変更した場合、次のエラーが出ることがある。

"Frameworks" ではちゃんと目的のフレームワークを指すように設定しているにもかかわらず出る。

これはビルド設定の「フレームワーク検索パス」が違っている場合がある。ターゲットの情報を開き「ビルド」タブの「フレームワーク検索パス」が正しいかどうかを確認する。
この設定は "Frameworks" へ登録した時に自動的に追加されるようだ。ただその後、"Frameworks"の方でパスを変更してもここには反映されない為に冒頭のエラーが出てている。

[Mac][iOS] Static Library (7) カテゴリを使う場合の注意点 "-ObjC" と "-all_load"

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

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

[前回] Cocoaの日々: [iOS] Static Library (6) OCUnitで単体テスト

カテゴリが定義されている Static Library を使う場合、リンカフラグに "-ObjC" と "all_load" を設定する必要がある。リンカフラグを設定しない場合 "selector not recognized" 例外が発生しクラッシュする。
[49296:207] -[NSCFString extString]: unrecognized selector sent to instance 0x3044
[49296:207] *** Terminating app due to uncaught exception 'NSInvalidArgumentException',
 reason: '-[NSCFString extString]: unrecognized selector sent to instance 0x3044'
*** Call stack at first throw:
このことは下記のQ&Aで解説されている。
この Q&A によればこの問題は UNIXの Static library実装 と Objective-C の動的な性質の違いによって引き起こされるとのこと。この問題を回避する為にリンカフラグに "-ObjC" オプションを追加する。

リンカフラグは、ターゲットの情報を開きビルドタブの「他のリンクフラグ」に設定する。
なお Mac OS X の64bit環境および iOS ではリンカにバグがあり、"-ObjC" フラグを指定してもカテゴリを含む Static Library のロードが働かない。これを回避する為に "-all_load" または "-force_load" フラグを使う。

"-all_load" フラグはリンカに全オブジェクトファイルのロードを指示する。"-force_load" は Xcode 3.2 から導入されたフラグで、オブジェクトファイル毎にロードを指示することができる。このフラグはオブジェクトファイルへのパスを引数に取る。


Duplicate Symbol 問題


[iPhone][Objective-C] staticライブラリにカテゴリを含む際に気をつけること によれば "-all_load" フラグも万能というわけではなく libtool のバグが原因で "Duplicate symbol" エラーを引き起こすケースがあるとのこと。この問題を回避するにはちょっとしたトリックを施すと良いらしい。以下、引用。
具体的には、以下のように関係ないクラスの宣言を.mの先頭の方に付加すればOK。「FIXCATEGORYBUG_NSSTRING_SAMPLE」は他と名前の衝突が無ければ何でもよいです。
  • NSString+Sample.m
@interface FIXCATEGORYBUG_NSSTRING_SAMPLE @end
@implementation FIXCATEGORYBUG_NSSTRING_SAMPLE @end

@implementation NSString (Sample)
 .
 .
 .
@end
この現象は確認できていないので今のところ何ともいえないがエラーが出た時に参考になりそうだ。


参考情報


IPHONE - Objective-C categories in static library - efreedom

use force_load - Three20 | Google グループ

[iOS] OCUnitで単体テスト〜アプリケーションテスト

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

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

[前回] Cocoaの日々: [iOS] Static Library (6) OCUnitで単体テスト

iOS Development Guide: Unit Testing Applications によればXcodeでOCUnit を使ったテストは2通りある。

  • Logical Tests
  • Application Tests

前回やったテストが Logical Tests にあたり、これはビルド時にテストを実行する。一方、Application Tests はビルド時ではなくアプリケーション実行時にテストを実行する。今回はこれを試してみた。

こんなプロジェクトを作る。ApplicationTestSampleTesting がアプリケーションテスト用ターゲット。
アプリを実行するとこんな内容がデバッグコンソールへ出力される。


手順


手順は iOS Development Guide: Unit Testing Applications に分かりやすく記載されている。今回は新規にプロジェクトを作成して試してみる。

1. 新規プロジェクト

View-based Application テンプレートを使って新規にプロジェクトを作成する。名前を ApplicationTestSample とした。


2. アプリケーションテスト用ターゲット作成

デフォルトで用意されるターゲットを複製する(右クリックで「複製」を選択)。名前は <アプリケーション名>Testing とする。


3. テストケース用ターゲット追加

続いてテストケース用ターゲット(バンドル)を追加する。「ターゲット」を右クリックして「追加」→「新規ターゲット」でダイアログを開き "Unit Test Bundle" を追加する。名前は "AppTests" とする(名前の最後を "Tests" としておく)。

次にこのターゲットをドラッグ&ドロップでアプリケーションテスト用ターゲット "ApplicationTestSampleTesting" に追加する。
さらい "Products" 内の "AppTests.octest" を "ApplicationTestSampleTesting" の「バンドルリソースをコピー」へドラッグ&ドロップで追加する。
これでアプリケーションにテストバンドルが格納され、実行時にテストが走るようになる。


4. テストコード作成

テストケース用のクラスを新規作成する。
名前を AppTest として追加先のターゲットは "AppTests" とする。
中身はこんな感じ。
#import <SenTestingKit/SenTestingKit.h&;gt;
#import <UIKit/UIKit>

@interface AppTests : SenTestCase {

}

@end
#import "AppTests.h"

@implementation AppTests

- (void) testFail {
 STFail(@"Must fail to succeed.");
}

@end

5. 実行

さて実行してみよう。実機を接続して実行する。

出た。デバッグコンソールに表示されるので非常に見づらい
Xcodeの「デバッグ」の設定でデバッグコンソールの色が変えると多少ましにはなる(あんまりかわらないか)。
こんな感じ。

なお、アプリケーションテストは実機でしか試せない。
シミュレータで実行しようとするとビルド時にテストコードが走ってしまいテストに失敗した場合は実行ができないので注意。これはリファレンスにちゃんと書いてあるのだが見落としていて最初はまった。


ソースコード


GitHub からどうぞ。
ApplicationTestSample at 2010-11-23 from xcatsan's iOS-Sample-Code - GitHub


参考情報


iOS Development Guide: Unit Testing Applications
単体テストの公式リファレンス

Cocoa with Love: A sample iPhone application with complete unit tests
公式リファレンスの補足的な資料となっている

[iOS] Static Library (5) Frameworkを作成する

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

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

[前回] Cocoaの日々: [iOS] Static Library (4) Universal Static Library

今回は iOS 向けに Framework を作成する。


Frameworkとは?


前回のエントリでまとめたのでそちらをどうぞ。
Cocoaの日々: [Mac][iOS] Frameworkとは?


サマリー


今回は SampleKit という Framework を作成する。

中身はこんな感じ。

Xcodeプロジェクトはこんな感じになる。

ターゲットを2つを用意する。 Frameworkを作成するターゲット(上図の SampleKit.framework ターゲット)を使いビルドを開始すると、最初に SampleKit ターゲットを呼び出して各アーキテクチャ毎のライブラリ(*.a)を作成する。

そしてこれらを lipoコマンドでユニバーサル化した後、Frameworkのディレクトリを構成する。最後に ZIP圧縮して配布できるようにする。


Frameworkで公開したいヘッダファイル(*.h)はHeaders に入れておく。これらがビルド時に Framework/Headers へコピーされる。公開したくないヘッダファイルはここへは入れない(Classesなどへ入れる)。

ビルド時に使用する Framework名称とバージョン番号(ZIPファイル名に使用)は Info.plist ファイル内の値を使う[*1]。

[*1] ビルド設定(環境変数)は使わない


Frameworkを作成した後、動作確認の為にサンプルアプリケーションを作る。



Framework 作成手順


1. Xcode プロジェクト作成


まずは Xcode を開き、テンプレートから "Cocoa Touch Static Library" を選択する。名前は "SampleKit" とした。
こんなプロジェクトが生成される。

2. リソース準備


Resources グループを作成し、Info.plist ファイルを作成する。
Info.plist の中身はこんな感じ。

上記はMac OS X 用フレームワーク作成テンプレートのものをコピーして、それにドキュメントで推奨されていた項目を付け足した( Copyright, Get Info stringなど)。Bundle Versions string, short と Bundle name はこの後のシェルスクリプトで参照する。


3. ヘッダ用ディレクトリ準備


公開ヘッダ用のディレクトリを準備する。まず Xcodeで新規にグループを作成し "Headers" と名前をつける。

ただこれは Xcodeプロジェクト上だけで有効なもので自分でディレクトリを作成してひもづける必要がある。"Headers"の情報を開き、パスの「選択」を選ぶ。

ダイアログが開いたら左下の「新規フォルダ」ボタンを押して "Headers" というディレクトリを作成する。
このディレクトリを選択すると情報のパスの欄に "Headers" と表示される。

この Headersディレクトリに入っているヘッダファイル(*.h)を後ほど Frameworkディレクトリへコピーする。公開したくないヘッダファイルがある場合はここへ入れなければ良い。



4. ソースコード準備


動作確認用のクラスのソースコードファイルを Classes 配下へ追加する。

今回 SampleClass.h は公開したい(Frameworkに同梱)ので Headers 配下に配置しておく逆に公開したくない場合は Classesなどへ入れておく。

中身はこんな感じ。
@interface SampleClass : NSObject {

}
- (NSString*)helloString;

@end
@implementation SampleClass

- (NSString*)stringHello
{
 return @"Hello mac!";
}
@end


5. ターゲット準備


Framework作成用に新規ターゲットを作成する。ターゲットの上で右クリックして「追加」→「新規ターゲット」→ "Shell Script Target" を選択する。
ターゲット名を "SampleKit.framework" とする。

ターゲット内の「スクリプトを実行」を開き、ビルド用のシェルスクリプトを記述する。

ビルド用シェルスクリプト
#--------------------------------------------------------------------
echo "[0] Framework: Preparing ..."
#--------------------------------------------------------------------
FRAMEWORK_NAME=$(/usr/libexec/PlistBuddy -c "Print CFBundleName" Info.plist)
BUILD_TARGET_NAME=$FRAMEWORK_NAME
FRAMEWORK_VERSION_NUMBER=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" Info.plist)
FRAMEWORK_VERSION=A
FRAMEWORK_BUILD_PATH="build/${BUILD_STYLE}-framework"
FRAMEWORK_DIR="${FRAMEWORK_BUILD_PATH}/${FRAMEWORK_NAME}.framework"
PACKAGENAME="${FRAMEWORK_NAME}.${FRAMEWORK_VERSION_NUMBER}.zip"

#--------------------------------------------------------------------
echo "[1] Framework: Building libraries ..."
#--------------------------------------------------------------------
xcodebuild -configuration ${BUILD_STYLE} -target ${BUILD_TARGET_NAME} clean
xcodebuild -configuration ${BUILD_STYLE} -target ${BUILD_TARGET_NAME} \
  -sdk iphonesimulator${IPHONEOS_DEPLOYMENT_TARGET}
[ $? != 0 ] && exit 1

xcodebuild -configuration ${BUILD_STYLE} -target ${BUILD_TARGET_NAME} \
  -sdk iphoneos${IPHONEOS_DEPLOYMENT_TARGET}
[ $? != 0 ] && exit 1

echo "Framework: Cleaning framework..."
[ -d "${FRAMEWORK_BUILD_PATH}" ] && rm -rf "${FRAMEWORK_BUILD_PATH}"

#--------------------------------------------------------------------
echo "[2] Framework: Setting up directories..."
#--------------------------------------------------------------------
mkdir -p ${FRAMEWORK_DIR}
mkdir -p ${FRAMEWORK_DIR}/Versions
mkdir -p ${FRAMEWORK_DIR}/Versions/${FRAMEWORK_VERSION}
mkdir -p ${FRAMEWORK_DIR}/Versions/${FRAMEWORK_VERSION}/Resources
mkdir -p ${FRAMEWORK_DIR}/Versions/${FRAMEWORK_VERSION}/Headers

#--------------------------------------------------------------------
echo "[3] Framework: Creating symlinks..."
#--------------------------------------------------------------------
ln -s ${FRAMEWORK_VERSION} ${FRAMEWORK_DIR}/Versions/Current
ln -s Versions/Current/Headers ${FRAMEWORK_DIR}/Headers
ln -s Versions/Current/Resources ${FRAMEWORK_DIR}/Resources
ln -s Versions/Current/${FRAMEWORK_NAME} ${FRAMEWORK_DIR}/${FRAMEWORK_NAME}

#--------------------------------------------------------------------
echo "[4] Framework: Creating library..."
#--------------------------------------------------------------------
lipo -create \
 build/${BUILD_STYLE}-iphoneos/lib${FRAMEWORK_NAME}.a \
 build/${BUILD_STYLE}-iphonesimulator/lib${FRAMEWORK_NAME}.a \
 -o "${FRAMEWORK_DIR}/Versions/Current/${FRAMEWORK_NAME}"

#--------------------------------------------------------------------
echo "[5] Framework: Copying assets into current version..."
#--------------------------------------------------------------------
cp Headers/*.h ${FRAMEWORK_DIR}/Headers/
cp Info.plist ${FRAMEWORK_DIR}/Resources/

#--------------------------------------------------------------------
echo "[6] Framework: Packaging framework..."
#--------------------------------------------------------------------
cd ${FRAMEWORK_BUILD_PATH}
zip -ry ${PACKAGENAME} $(basename $FRAMEWORK_DIR)
(2011-09-05追記)
Xcode 4.1 から環境変数 BUILD_STYLE が使われなくなった。代わりに CONFIGURATION を使う。上記スクリプト内の ${BUILD_STYLE} をすべて ${CONFIGURATION} で置換する。

(2011-10-21追記)
Xcode 4.2 から SDK 5.0 のみの提供となった。この為、ターゲットを 4.3 としてビルドするとエラーになる。その場合は xcodebuild コマンドの引数から ${IPHONEOS_DEPLOYMENT_TARGET} を取り除く。
(修正前)xcodebuild -configuration ${BUILD_STYLE} -target ${BUILD_TARGET_NAME} \
  -sdk iphoneos${IPHONEOS_DEPLOYMENT_TARGET}
(修正後)xcodebuild -configuration ${BUILD_STYLE} -target ${BUILD_TARGET_NAME} \
  -sdk iphoneos


(2011-10-21追記)
Xcode 4.2 でターゲットを 4.3 としてビルドするとエラーが出る場合がある。この場合はコンパイラを Apple LLVM compiler 3.0 から LLVM GCC 4.2 に切り替えると解消する(かもしれない)。


xcodebuild コマンドを使い実機(iphoneos)とシミュレータ(iphonesimulator)用のバイナリ(*.a) ファイルを作成する。その後、lipo コマンドで1つにまとめ(ユニバーサル化)Frameworkのディレクトリを構成する。最後はそのディレクトリを ZIP圧縮する。

6. ビルド


ビルドしてみよう。"SampleKit.framework" を選択しビルドを実行する。
※Device/Simulator は無視される。Debug/Release は有効。

フォルダを確認すると...
できた。

file コマンドでバイナリを確認してみる。
$ file SampleKit 
SampleKit: Mach-O universal binary with 3 architectures
SampleKit (for architecture armv6): current ar archive random library
SampleKit (for architecture armv7): current ar archive random library
SampleKit (for architecture i386): current ar archive random library
ちゃんと arm6/7、i386用のライブラリが入っている。


Frameworkを利用するサンプル


作成した Framework を使うサンプルを作ってみる。View-basedなプロジェクトを作成し、先ほど作成した Frameworkを追加する。
Frameworkとして認識された。
Xcodeのウィンドウの左ツリーで SampleKit.frameworkを選択すると右側には内包している SampleClass.h が表示される。

コントローラに下記コードを追加する。
#import <SampleKit/SampleClass.h>
  :
- (void)viewDidLoad {
    [super viewDidLoad];
 
 SampleClass* obj = [[[SampleClass alloc] init] autorelease];
 NSLog(@"%@", [obj stringHello]);
}
Framework 内のヘッダファイルは下記の形式でインポートする。
#import <フレームワーク名/ヘッダファイル名>

シミュレータで実行すると...
SampleKitClient[19559:207] Hello mac!
デバッグコンソールに出た。その後、実機でも動作確認ができた。良さそうだ。


考察


"Frameworkとは?" の記事で触れた様に iOS の場合は Framework は静的ライブラリのプレースホルダとして使うのが一般的と思われる。


運用イメージ

この為、もし自作した Framework がリソース(nib,画像など)を含む場合は、利用側のプロジェクトのビルドフェーズでそれらをアプリケーションバンドル内へコピーして一緒に配布する必要がある。

なお仕組み自体は動的ライブラリの格納も可能なので、"Frameworkとは?"で取り上げた embed形態を取ることで実行時にリンクさせることもできる。この場合だと実行時に必要なライブラリしか読み込まれない為、フットプリント(メモリ消費)を減らせる可能性がある。



トラブルシュート


SDKが見つからないエラーが出た場合



スクリプト内のPATH環境変数設定で古い SDKのディレクトリを指定している場合など。
export PATH=/Developer/usr/bin:"$PATH"
明示的に指定する必要がなければ、この PATH設定を削除しておく。すると現在実行中の Xcodeの SDKのバージョンが使用される

ビルド中にエラー


ターゲットが見つからない場合などは、スクリプト内の xcodebuild コマンドで指定しているターゲットが正しく設定されていない可能性がある。

スクリプト内の xcodebuildコマンドのターゲットに指定しているのは、最初にテンプレートで作成されたターゲットである "SampleKit" である。このターゲット名を用意するのに Info.plist 内の "Bundle Name" を使用している。この名前とライブラリ(*.a)作成用のターゲット名が一致している必要がある。
シェルスクリプト内の該当箇所は下記になる。
#--------------------------------------------------------------------
echo "[0] Framework: Preparing ..."
#--------------------------------------------------------------------
FRAMEWORK_NAME=$(/usr/libexec/PlistBuddy -c "Print CFBundleName" Info.plist)  // ←これ
BUILD_TARGET_NAME=$FRAMEWORK_NAME  // ←

ヘッダが無いというエラーが出る


Headers ディレクトリにファイルが無いとこのエラーが出る。例えば追加したヘッダファイルのパスが間違っていた場合など。
この場合は該当するファイルが入っているフォルダを開く。
そしてファイルを Headers へコピーする。
すると Xcode 上でファイルが赤くなる。これは指定パス位置にファイルが無いことを示している。
そこで情報を開き、パスの「選択」ボタンを押して目的のファイルを選択する。
これでビルドが通る様になる。


ソースコード


GitHubからどうぞ。

Framework作成プロジェクト
SampleKit at 2010-11-19c from xcatsan's iOS-Sample-Code - GitHub

Frameworkを使うサンプル
SampleKitClient at 2010-11-19 from xcatsan's iOS-Sample-Code - GitHub

※ SampleKitClient 内で参照している SampleKit へのパスは私のPCのローカル環境になっています。ビルドする際には SampleKit を別途ダウンロードして、参照先をそちらに切り替えて下さい。


備考:コマンド


lipo


lipo(1) Mac OS X Manual Page

複数のアーキテクチャ向けのライブラリやアプリケーションバイナリを1つにまとめることができるツール。

[参考情報]
Undocumented Mac OS X:第13回 Universal Binary【後編】 (2/4) - ITmedia エンタープライズ
yebo blog: ユニバーサルバイナリ化するツール lipo

PlistBuddy(8) Mac OS X Manual Page


PlistBuddy


*.plist ファイルの値を設定したり取得したりできるコマンド。/usr/libexec/配下に存在する。
# CFBundleName 取得例
FRAMEWORK_NAME=$(/usr/libexec/PlistBuddy -c "Print CFBundleName" Info.plist)

PlistBuddy を使ってバージョン番号を自動的にインクリメントするアイディアも公開されている。
Incrementing Build Numbers in Xcode | Dave DeLong


備考


今回 PlistBuddy を使い、シェルスクリプトから Info.plist 内の情報を取り出してビルドに使用した。本来であればビルド設定の値(環境変数に設定される)を使うべきで、一般的な Xcodeプロジェクトではそのようになっている。Info.plist 自体にも環境変数が埋めこまれていてビルド時にこれが置換される。下記は Mac OS X 用 Framework テンプレートで生成された Info.plist。
${EXECUTABLE_NAME} など環境変数が使われているのがわかる。Info.plist 内の環境変数の置換はビルド時に行われている。以下は Mac OS X 用Framework をビルドしている時のメッセージ。
メッセージに表示されている builtin-infoPlistUtility は外部コマンドとして存在するものではなくて、Xcode内部の機能として存在するようだ。先頭にある "ProcessInfoPlistFile" はビルドのルール設定項目と思われるが見つからなかった(図にあるように CopyPlistFile というルールは設定項目にある)。
また今回使用しているターゲットタイプ(Shell Script)ではルールが編集できない。
仕方ないので
Info.plist ==(参照)==> 環境変数
シェルスクリプト ==(参照)==> 環境変数
をやめて
Info.plist をオリジナル
シェルスクリプト ==(参照)==> Info.plist
とした。


Framework 参考情報


iOS向けの Framework に関しては Appleからドキュメントが提供されていない。この為、多くの人が自分たちで試行錯誤して Framework作成を試みている。そのおかげもあって Framework作成はだいたいすんなり行うことができた。情報提供してくれた方々にはこの場を借りてお礼を申し上げます。

iPhone アプリ開発で使える Framework の作り方 | KRAY Inc
Framework の作成方法が図入りで丁寧に解説されている。とても参考になった。

[Mac][iPhone][develop] iPhone OS用のほぼFrameworkの作り方 - Ni chicha, ni limona -平均から抜けられない僕-
ライブラリファイル(*.a)を lipo を使ってユニバーサル化する方法の紹介。

Mac♪Mac♪Mac♪ - 第11回 フレームワークを作成する
Mac OS X でフレームワークを作成する解説。

How to (almost) create your own iPhone OS framework

Making Your Own iPhone Frameworks @ Cocoanetics

“Making Your Own iPhone Frameworks”

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