Hatena::ブログ(Diary)

中継地点 RSSフィード Twitter

2011-08-20

FacebookAPI (iOS SDK)を拡張する

iPhone Dev THK(東北・仙台)勉強会セッションを行いました。Facebook APIネイティブアプリとして動かすには幾つかのハードルがあり、実際にアプリに組み込んで使うにはiOS SDKを拡張しないと現実的でないので、その拡張をライブコーディングで行いました。



Facebook APIをラップしたiOSモジュールが公式に配布されているのでそれをgithubから取得します。取得したモジュールの中にDemoAppというプロジェクトがあるので、これを使って色々と動作の確認が行えます。

実際にDemoAppを動かすには、Facebookアプリを登録してAPI Keyを取得する必要がありますが、アプリを登録するには、まずFacebook Developersで自分のFacebookアカウントを開発者として登録が必要になります。

(※登録には携帯電話クレジットカードが必要)


アプリの登録は「アプリ」タブの「Create New App」から登録します。アプリ登録が完了したら、概要にAPI Keyが表示されますので、このキーを DemoAppViewController.m の kAppId に設定します。

※token secretは使用しなくてもアクセス可能


f:id:h_mori:20110821012116p:image


DemoAppViewController.m

// Your Facebook APP Id must be set before running this example
// See http://www.facebook.com/developers/createapp.php
// Also, your application must bind to the fb[app_id]:// URL
// scheme (substitute [app_id] for your real Facebook app id).
static NSString* kAppId = @"23xxxxxxxxxxxxx";

初期設定では認証が標準のSafariで行うようになっており、認証後のコールバックにURL Schemeを使います。


DemoApp-Info.plist


f:id:h_mori:20110821012117p:image

URL Schemes > Item 0 にはfb+API Key(数字) となります。



これで一通り動かせるようになるのですが、毎回Safariで認証してコールバックさせるのは現実的ではないのでFacebookが用意しているダイアログモードで認証させるようにします。

実際に認証の処理を実行するメソッドFacebook#authorizeWithFBAppAuth:safariAuth: で行われ、この第2引数Safariモードにするかダイアログモードにするかを決定しています。デフォルトがYESとなっているのでこれをNOに変更します。


Facebook.m

- (void)authorize:(NSArray *)permissions
       localAppId:(NSString *)localAppId {
    self.localAppId = localAppId;
    self.permissions = permissions;
    
    [self authorizeWithFBAppAuth:YES safariAuth:NO];
}

これでSafariに飛ばずにダイアログのWebViewで認証が行えるようになります。

Facebook.mはSDKコアなので、直接変更するのに違和感があるようであればカテゴリオーバーライド等で塗りつぶすのがいいかもしれません。


もう一つの難点として、Facebookのaccess_tokenは有効期限が短く切れてしまうためにアプリ上では毎回認証をさせる必要があります。DemoAppでは毎回Loginボタンを押す必要があるのでこれを起動時に自動で認証させるようにします。


先ほどの Facebook#authorizeWithFBAppAuth:safariAuth:が認証をさせるためのリクエストを発行していますが、このログイン用リクエストは認証済みであればWeb画面を返さずに自動的にリダイレクトでaccess_tokenを投げ返すようになっています。これを利用してダイアログを出さずにリクエストだけ発行する仕組みを作れば自動認証が可能となります。

具体的には FBLoginDialog#show を行わずに FBLoginDialog#load だけを実行すればダイアログなしにリクエストの発行が行えます。

ここではダイアログなしで認証するカテゴリメソッドを追加して対応します。

Facebook+Add.h

#import <Foundation/Foundation.h>
#import "Facebook.h"

@interface Facebook (Add)
- (void)authorizeWithoutDialog;
@end

Facebook+Add.m

#import "Facebook+Add.h"

@implementation Facebook (Add)
static NSString* kDialogBaseURL = @"https://m.facebook.com/dialog/";
static NSString* kRedirectURL = @"fbconnect://success";
static NSString* kLogin = @"oauth";
static NSString* kSDKVersion = @"2";

/**
 * A public function for authorization without dialog.
 */
- (void)authorizeWithoutDialog {
    NSMutableDictionary* params = [NSMutableDictionary dictionaryWithObjectsAndKeys:
                                   _appId, @"client_id",
                                   @"user_agent", @"type",
                                   kRedirectURL, @"redirect_uri",
                                   @"touch", @"display",
                                   kSDKVersion, @"sdk",
                                   nil];
    NSString *loginDialogURL = [kDialogBaseURL stringByAppendingString:kLogin];
    if (_permissions != nil) {
        NSString* scope = [_permissions componentsJoinedByString:@","];
        [params setValue:scope forKey:@"scope"];
    }
    if (_localAppId) {
        [params setValue:_localAppId forKey:@"local_client_id"];
    }
    
    // If single sign-on failed, open an inline login dialog. This will require the user to
    // enter his or her credentials.
    [_loginDialog release];
    _loginDialog = [[FBLoginDialog alloc] initWithURL:loginDialogURL
                                          loginParams:params
                                             delegate:self];
    [_loginDialog load];
}
@end

後は、起動直後、ここでは DemoAppViewController#viewDidLoad で 追加したメソッドをコールするようにしました。


DemoAppViewController.m

/**
 * Set initial view
 */
- (void)viewDidLoad {
    [self.label setText:@"Please log in"];
    _getUserInfoButton.hidden = YES;
    _getPublicInfoButton.hidden = YES;
    _publishButton.hidden = YES;
    _uploadPhotoButton.hidden = YES;
    _fbButton.isLoggedIn = NO;
    [_fbButton updateImage];
    
    [_facebook authorizeWithoutDialog];
}

これで一度認証を行えば、起動時に自動的にログインを行うようになります。(※Facebook認証サーバの負荷状況によっては若干時間がかかる場合があるようです)


他にも不便な点として、コールバック関数であるFBRequestDelegateの request:didLoad: の引数が直接resultのみでFacebookからのレスポンス(jsonデータ)には、リクエスト時の情報がほとんど入っておらず、非同期でガシガシとアクセスした場合に、どのリクエストに対したレスポンスかわからない状態となってしまいます。

これを解消するためには、FBRequest#handleResponseData をオーバーライドする必要がありますが大掛かりな変更となるので、また別のエントリで行ないたいと思います。


今回、勉強会で使用したソースコードgithubにアップしました。

2011-08-13

Xcode4のデバッグコンソールを別ウィンドウにする

Xcode4になってからデバッグウィンドウやコンソールウィンドウ等が1ウィンドウに統合されました。InterfaceBuilderやInstrumentsもXcode4に統合され、1つにまとまった感を印象付ける内容ですが、Xcode3のデバッグ、コンソールウィンドウは分かれてたからこそ2画面での開発時などで便利だった面があったりします。

色々と設定を探してみましたが、分離する項目は見当たらなかったのですが、新しくウィンドウを作ってコンソールウィンドウのみにすることでとりあえずは暫定的に対処できそうです。



f:id:h_mori:20110813053226p:image


Xcodeメニューの「File > New Window」(Shift+コマンド+T) で新しいウィンドウを開き、Viewを真ん中だけをONにします。

デバッグウィンドウのバーの部分をダブルクリックすれば全画面になります。

続けてXcodeメニューの「View > Hide Toolbar」を選択するとウィンドウももう少し小さくサイズが変更できるようです。


f:id:h_mori:20110813055704p:image



実行時にメインウィンドウから自動的に出てくるデバッグコンソールを消す場合は、「Preferences」からBehaviorsタブのRun ... の「Show Debug Area」のチェックを外します。


f:id:h_mori:20110813055705p:image



不満な点は、Xcode4終了時にQuitで閉じる場合(コマンド+Q)は問題ないのですが、ウィンドウを閉じる(コマンド+W)の場合にコンソールを最後に残してしまうと、Xcode4の最終状態がコンソールウィンドウの状態になってしまうため、次に開くとちょっとガッカリな感じになってしまいます。

やはりウィンドウ分離モードが素直に欲しいと思いますが、何かやり方があるんでしょうか?

2011-08-09

Xcode4を軽くする

ようやくXcode4で機能的には統合環境としてマトモになってきたかと思いますが、Xcode3に比べ格段に重くなっています。普段はCore2Duo 2GHz メモリ4Gのマシンで開発していますが、これでもXcode4を毎回再起動しながら開発している状況です。色々と設定を変えてみたら、まだ再起動は必要ですが若干快適になった気がします。



自動保存・自動コンパイルをしない

f:id:h_mori:20110808214333p:image

Xcode > Preferences のGeneralタブで Auto-Saveを「Never」、Enable Live Issuesの各チェックを外す



Source Version Control 対象外にする

f:id:h_mori:20110808214334p:image

File > Source Control > Repositries でリポジトリを選択して「-」で削除する

(※Xcode再起動すると復活してしまうので毎回行う必要があります)


後、プロジェクト内のxcworkspaceを消すと軽くなるとか情報がありましたが、あまり実感できませんでした。

2011-08-06

iPhoneのマイクで一定以上の大きさの音を検知する

Hack for Japan 7/30 ハッカソン仙台会場で「堪忍袋」というアプリの根幹を実装しました。

アイディアとして、いびきなどの音に反応して爆発音を鳴らすという単純な仕掛けですが、

肝はマイクで一定以上の音の大きさを検知することが必須となります。

今回は、AudioToolboxのAudioQueueServicesを使い、AudioQueueLevelMeterStateのpeakPowerをスレッドで監視させて実装しました。


SoundPickerViewController.h

#import <UIKit/UIKit.h>
#import <AudioToolbox/AudioToolbox.h>

@interface SoundPickerViewController : UIViewController {
    AudioQueueRef queue;  
}
@end


SoundPickerViewController.m

@interface SoundPickerViewController ()
- (void)start;
- (void)fire;
@end

@implementation SoundPickerViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    [self start];
}

static void AudioInputCallback(  
                               void* inUserData,  
                               AudioQueueRef inAQ,  
                               AudioQueueBufferRef inBuffer,  
                               const AudioTimeStamp *inStartTime,  
                               UInt32 inNumberPacketDescriptions,  
                               const AudioStreamPacketDescription *inPacketDescs) {  
}

- (void)start {
    AudioStreamBasicDescription dataFormat;  
    dataFormat.mSampleRate = 44100.0f;  
    dataFormat.mFormatID = kAudioFormatLinearPCM;  
    dataFormat.mFormatFlags = kLinearPCMFormatFlagIsBigEndian | kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
    dataFormat.mBytesPerPacket = 2;  
    dataFormat.mFramesPerPacket = 1;  
    dataFormat.mBytesPerFrame = 2;  
    dataFormat.mChannelsPerFrame = 1;  
    dataFormat.mBitsPerChannel = 16;  
    dataFormat.mReserved = 0;  
    
    AudioQueueNewInput(&dataFormat,AudioInputCallback,self,CFRunLoopGetCurrent(),kCFRunLoopCommonModes,0,&queue);  
    AudioQueueStart(queue, NULL);
    
    UInt32 enabledLevelMeter = true;  
    AudioQueueSetProperty(queue,kAudioQueueProperty_EnableLevelMetering,&enabledLevelMeter,sizeof(UInt32));
    
    [NSTimer scheduledTimerWithTimeInterval:0.2 
                                     target:self 
                                   selector:@selector(updateVolume:) 
                                   userInfo:nil 
                                    repeats:YES];
}

- (void)updateVolume:(NSTimer *)timer {
    AudioQueueLevelMeterState levelMeter;  
    UInt32 levelMeterSize = sizeof(AudioQueueLevelMeterState);  
    AudioQueueGetProperty(queue,kAudioQueueProperty_CurrentLevelMeterDB,&levelMeter,&levelMeterSize);

    NSLog(@"mPeakPower=%0.9f", levelMeter.mPeakPower);
    NSLog(@"mAveragePower=%0.9f", levelMeter.mAveragePower);
    
    if (levelMeter.mPeakPower >= -1.0f) {
        [self fire];
    }
}

- (void)fire {
    NSLog(@"fire!");
}
@end

音を拾うためにAudioQueueをスタートさせますが、その際にAudioQueueSetPropertyでkAudioQueueProperty_EnableLevelMeteringをONにします。

後は、0.2秒毎にAudioQueueのproperty、kAudioQueueProperty_CurrentLevelMeterDBの値をAudioQueueLevelMeterStateの構造体で受け取ります。構造体の中はmPeakPowerとmAveragePowerがあり、今回はPeakPowerが-1.0以上を検知するようにしました。

尚、powerはFloat32ですが最大値は0で通常は負の値となります。


録音するなどの必要はないので、今回AudioInputCallbackは実装していません。

単に音をハンドリングとしたいだけなのに、ちょっと野暮ったい気もします。