Category Archives: Objective-c

こんにちはこんばんわ!azuの記事に触発されて使ってみる事にしました!!!

■ダウンロード

gh-unit -> Downloadsで特別な理由がない場合は最新版を使いましょう。

■ターゲットの追加

Project navigatorでProjectを選択し、Add Targetをクリックしてください。

次に、Window Based ApplicationでProduct NameをTestsとして適当にターゲットを追加します。

frameworkの追加

Project navigatorでProjectを選択し、TargetでTestsを選択した後、Build PhasesからLink Binary With Librariesを開き、ダウンロードしてできたframeworkを追加してください。

Info.plist

Test/Supporting Files/Test-Info.plistを選択してMain nib file base nameの属性を削除します。

ファイルの削除

Test配下の以下のファイルを削除してください。

  • TestAppDelegate.h
  • TestAppDelegate.m
  • MainWindow.xib
  • Supporting Files以外のディレクトリ

■リンク

Project navigatorでProjectを選択し、TargetでTestsを選択した後、Build SettingからOther Linker Flagを選択し、以下の記述を加えてください。

  • -ObjC
  • -all_load

■Test/main.m

以下の部分を

int retVal = UIApplicationMain(argc, argv, nil, nil);

以下のように変更します。

int retVal = UIApplicationMain(argc, argv, nil, @"GHUnitIOSAppDelegate");

■テストケース

Test配下に書いていきす。但し、ターゲットはTestsにしてファイルを新しく追加します。

Test/MyTest.m

以下のように試しに書いてみました。

#import <GHUnitIOS/GHUnit.h>
@interface MyTest : GHTestCase {  
}
@end

@implementation MyTest
- (void)testStrings {
    NSString *string1 = @"a string";
    GHTestLog(@"I can log to the GHUnit test console: %@", string1);
   
    // Assert string1 is not NULL, with no custom error description
    GHAssertNotNULL(string1, nil);
   
    // Assert equal objects, add custom error description
    NSString *string2 = @"a string";
    GHAssertEqualObjects(string1, string2, @"A custom error message. string1 should be equal to: %@.", string2);
    GHAssertEquals(1, 1, @"test");
}

// わざとエラーを出してみる
- (void)testNums {
    GHAssertEquals(1, 2, @"test");
}
@end

テストメソッド名はtestから始めなくてはいけないですね。ちなみにGHAssertまで入力すれば大体どんなアサーションがあるのか確認できます。

■実際のテスト

ほとんどの記事を見ても実際のテストコードに近いコードがみつかりませんでした。困ったので実際に使えそうなコードを自力で書いてみました。

Util.m

Utilクラスです。URLエンコードしたりデコードしたりするメソッドがあります。

#import <Foundation/Foundation.h>
@interface Util : NSObject {
}
+ (NSString *)urlencode:(NSString *)text;
+ (NSString *)urldecode:(NSString *)text;
@end

@implementation Util
//encode
+ (NSString *)urlencode:(NSString *)text
{
    return (NSString *)CFURLCreateStringByAddingPercentEscapes(NULL,(CFStringRef)text,NULL,(CFStringRef)@"!*'();:@&=+$,/?%#[]",kCFStringEncodingUTF8);
}

// decode
+ (NSString *)urldecode:(NSString *)text
{
    return (NSString *)CFURLCreateStringByReplacingPercentEscapesUsingEncoding(NULL,(CFStringRef) text,CFSTR(""),kCFStringEncodingUTF8);
}
@end

注意点としては、テストクラスからインポートする為に、Util.mのファイルのターゲットにTestsを加える点です。

UtilTest.m

#import <GHUnitIOS/GHUnit.h>
#import "Util.h"
@interface UtilTest : GHTestCase {
}
@end

@implementation UtilTest
- (void)test_urlencode
{
    NSString *url1 = [NSString stringWithString:@"%E3%83%86%E3%82%B9%E3%83%88%E3%82%B1%E3%83%BC%E3%82%B9"];
    NSString *url2 = [NSString stringWithString:[Util urlencode:@"テストケース"]];
    GHAssertEqualObjects(url1, url2, @"match!");
   
    NSString *url3 = [NSString stringWithString:@"%E9%9A%A3%E4%BA%BA%E3%81%AF%E7%BE%8E%E4%BA%BA"];
    NSString *url4 = [NSString stringWithString:[Util urlencode:@"隣人は美人"]];
    GHAssertEqualObjects(url3, url4, @"match!");
}

- (void)test_urldecode
{
    NSString *url1 = [NSString stringWithString:[Util urldecode:@"%E3%83%86%E3%82%B9%E3%83%88%E3%82%B1%E3%83%BC%E3%82%B9"]];
    NSString *url2 = [NSString stringWithString:@"テストケース"];
    GHAssertEqualObjects(url1, url2, @"match!");
   
    NSString *url3 = [NSString stringWithString:[Util urldecode:@"%E9%9A%A3%E4%BA%BA%E3%81%AF%E7%BE%8E%E4%BA%BA"]];
    NSString *url4 = [NSString stringWithString:@"隣人は美人"];
    GHAssertEqualObjects(url3, url4, @"match!");
}
@end

これでTestsをターゲットにRunすればテストができますね。ちなみにですが、テストクラスの名前にはTestがつかなくても特に問題ないようです。

参考

■まとめ

殆ど公式ドキュメントと変わりませんが日本語がスキな方に読んでいただければと思います。

それぞれについての簡単な導入方法です。
まずは標準で入っているSenTestingKitから。
(非同期なテストができなかったり、使いにくいとかいろいろ言われていますが)
サンプルが配布されているので、それを使ってみるのが楽です。

公式に配布されているサンプルはXCodeから直接開くことができます。
XCodeのスタート画面 -> Learn about using XCode -> iPhoneUnitTestsで検索 -> Open Project

NewImageNewImage

電卓アプリでSenTestingKitの基本的なテスト方法がわかる。

テストのやり方についてはReadme.txtに書いてあって、
  1. メニューバーから〜TestのiPhone4.3エミュレーターなどを選ぶ
  2. ProductからTestを実行する
  3. View > Navigators > Log にテスト結果がでる。
  4. 成功の場合はログにでるだけ、失敗の場合はテストの失敗した場所がRedになる

オールグリーンの時は何も表示されない(ログだけなのが残念な感じ)

Xcode4でのユニットテストもGHUnitが良い | CAPH TECH

非同期テストができて、アプリとして起動するテストフレームワーク
導入は公式のGHUnit: Installingが一番わかり易い。
導入でハマる可能性がある所はOther Linker optionぐらいかも。
導入して、テストケースを書くときに使うテンプレートはtiboll/GHUnit-Template at master – GitHubから拝借。
  1. ~/Library/Developer/Xcode/ に Templatesディレクトリを作成
  2. GHUnit Test Case.xctemplate ディレクトリごと 先ほど作成したTemplatesフォルダに入れる
  3. New Fileにテンプレートが加わってる
GHUnitは奥が深く人気もあるので、多分メインなものになりそうです。
BDDなテストフレームワークとしてKiwiも気になる。

こんにちはこんばんは!暑いですね!~“Q。(‘‐’。)” パタパタ。凄く暑いですね。暑いのでiPhoneのネイティブアプリでFacebookにログインするアプリが作りたくなりました。無茶な流れですが暑いので許してください。

■インポート

ソースを取得

以下のコマンドでgithubから取得できます。

git clone git://github.com/facebook/facebook-ios-sdk.git

SDKをgithubで配布ですよ!日本企業も見習わなくちゃいけませんね!持ってきたディレクトリのsrcディレクトリに必要なファイルは揃っていますので、そのままプロジェクトに突っ込みましょう。(.xcodeprojなどは不要です)

■ログインの実装

AppDelegate.h

以下のようにFBConnect.hをインポートし、FBSessionDelegateを実装します。

#import <UIKit/UIKit.h>
#import "FBConnect.h"

@class MainViewController;

@interface facebookAppDelegate : NSObject <UIApplicationDelegate, FBSessionDelegate> {
    Facebook *facebook;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) Facebook *facebook;
@property (nonatomic, retain) MainViewController *mainViewController;
@end

AppDelegate.m

#import "facebookAppDelegate.h"

/**
 * @see https://developers.facebook.com/apps/
 * アプリを登録すると発行されます(公開までsandboxモードにしておきましょう)
 */

#define APP_ID @"123456789"

@implementation facebookAppDelegate

@synthesize window=_window;
@synthesize facebook;
@synthesize mainViewController;

- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url {
    return [facebook handleOpenURL:url];
}

// ブラウザのログイン画面から戻ってきた時に実行される
// ログイン情報をNSUserDefaultsに保存する
- (void)fbDidLogin {
    NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
    [ud setObject:[facebook accessToken] forKey:@"FBAccessTokenKey"];
    [ud setObject:[facebook expirationDate] forKey:@"FBExpirationDateKey"];
    [ud synchronize];
   
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // APP IDをセットしてインスタンス化
    facebook = [[Facebook alloc] initWithAppId:APP_ID];

    // ログイン情報があればセット
    NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
    if ([ud objectForKey:@"FBAccessTokenKey"] && [ud objectForKey:@"FBExpirationDateKey"]) {
        [facebook setAccessToken:[ud objectForKey:@"FBAccessTokenKey"]];
        [facebook setExpirationDate:[ud objectForKey:@"FBExpirationDateKey"]];
    }
   
    // パーミッションをセットして有効なセッションがなければブラウザでログインする
    NSArray* permissions =  [[NSArray arrayWithObjects:@"email", @"read_stream", nil] retain];
    if (![facebook isSessionValid]) {
        [facebook authorize:permissions delegate:self];
    }
    [self.window makeKeyAndVisible];
    return YES;
}

info.plist

Facebook SDKではinfo.plistに記述したURLによってアプリケーションを起動できる仕組みを使っています。以下のように変更してください。

「(Array) URL types > (Dictionary) Item 0 > (Array) URL Schemes > (Dictionary) Item 0 > (String) fb[APP ID]」となるように入力します。

起動

起動するとちゃんとログインができるかと思います。

■API

まぁ、ログインだけというのもなんなのでGraph APIを叩いてみましょう。

AppDelegate.h

FBRequestDelegateを実装します。

#import <UIKit/UIKit.h>
#import "FBConnect.h"

@class MainViewController;

@interface facebookAppDelegate : NSObject <UIApplicationDelegate, FBSessionDelegate, FBRequestDelegate> {
    Facebook *facebook;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) Facebook *facebook;
@property (nonatomic, retain) MainViewController *mainViewController;
@end

AppDelegate.m

makeKeyAndVisibleの前あたりでrequestWithGraphPathを呼びます。

    [facebook requestWithGraphPath:@"me" andDelegate:self];
    [self.window makeKeyAndVisible];
結果を受け取る

以下の4メソッドをAppDelegate.mに実装することで、NSURLConnection的に結果を受け取れます。

/**
 * Called just before the request is sent to the server.
 */

- (void)requestLoading:(FBRequest *)request
{
   
}

/**
 * Called when the server responds and begins to send back data.
 */

- (void)request:(FBRequest *)request didReceiveResponse:(NSURLResponse *)response
{
   
}

/**
 * Called when an error prevents the request from completing successfully.
 */

- (void)request:(FBRequest *)request didFailWithError:(NSError *)error
{
   
}

/**
 * Called when a request returns and its response has been parsed into
 * an object.
 *
 * The resulting object may be a dictionary, an array, a string, or a number,
 * depending on thee format of the API response.
 */

- (void)request:(FBRequest *)request didLoad:(id)result
{
    NSLog(@"%@", result);
}

/**
 * Called when a request returns a response.
 *
 * The result object is the raw response from the server of type NSData
 */

- (void)request:(FBRequest *)request didLoadRawResponse:(NSData *)data
{
   
}

以上となります。公式ドキュメントを翻訳しただけのようになってしまいましたが暑いので許してください?

■参考

Mobile Apps

みなさんこんにちは、こんばんは!すっかり夏になりました。iPhoneからHTTP通信したくなる季節ですね!かなり無茶な流れですが今回はiOSにおけるHTTP通信をトピックとさせて頂きます。

NSURLConnectionを使っても良いのですが、素晴らしく楽なのでASIHTTPRequestというライブラリを使います。

■ダウンロード

こちらからダウンロードした後、解凍しプロジェクトに展開します。

ビルド

試しにビルドすると凄まじいほどにエラーが出ますので必要なライブラリなどを準備しましょう。

■セットアップ

Reachability

アップルが配布してるのでダウンロードして、Reachability.hとReachability.mをプロジェクトに取り込んでください。ASIHTTPRequestを使用する上で必要になります。

以下のフレームワーク・ライブラリが必要になりますのでリンクしてください。

  • SystemConfiguration.framework

ASIHTTPRequest

以下のフレームワーク・ライブラリが必要になりますのでリンクしてください。

  • CFNetwork.framework
  • MobileCoreService.framework
  • libz.dylib

ASIHTTPRequestをダウンロードしてきたディレクトリにサブディレクトリがあるのですが、それも含めてビルドする場合はさらに以下のフレームワーク・ライブラリが必要になります。

  • libxml2.dylib

Build Settings > Search Paths > Header Search Pathsに「$SDKROOT/usr/include/libxml2」を追記してください。

ちなみにTestディレクトリも含める場合は以下のフレームワークが必要になるようです。

ただフレームワーク内部でNSTaskを使っているようなのですが、もはやiOSには含まれていないらしく上手くビルド出来ませんでした。今回はTest以外のディレクトリで話を進めていきます。誰か分かる人いたら教えて下さい。

■実装

以下のようにする事でGETリクエスト(非同期)する事ができます。

ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:url]];
[request setDelegate:self];
[request setRequestMethod:@"GET"];
[request startAsynchronous];

以下のようにする事でPOSTリクエスト(非同期)する事ができます。

ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:[NSURL URLWithString:url]];
[request setDelegate:self];
[request setPostValue:@"this is a pen." forKey:@"key"];
[request startAsynchronous];

通信結果を受け取る

非同期で通信した結果をASIHTTPRequestDelegateで受け取ります。

#pragma mark -
#pragma mark ASIHTTPRequestDelegate
- (void)requestFinished:(ASIHTTPRequest *)request {
    SBJSON *parser = [[SBJSON alloc] init];
    NSDictionary *json = [parser objectWithString:[request responseString] error:nil];
    [parser release];
}
- (void)requestFailed:(ASIHTTPRequest *)request {
    NSError *error = [request error];
    NSLog(@"%@", error);
    // 通信ができなかったときの処理
}

通信して取得した文字列は、[request responseString]に入ってますので煮たり焼いたりして使いましょう。上述のコードはJSONと仮定し、SBJSONというライブラリを使ってパースしています。

■まとめ

どうでしょうか?とってもカンタンですね!色々な事をやるあまり昔やった事を忘れかけてるのでメモしておきました’`,、’`,、ヾ(o´∀`o)ノ ‘`,、’`,、・・・あ、NSURLConnectionも参考に書こうかと思いましたが、アレがアレなのでまた次回にします。

みなさんこんにちは。こんばんわ。スマートフォン事業部のイッシーです。スマートフォンアプリを開発しているスマートフォン事業部のイッシーです。スマートフォンという単語を乱発してますが今回はスマートフォンの話だからです。ʕ→ᴥ←ʔ

今回はiPhoneにおけるアプリ内課金のお話です。なんか敷居が高いですね!でも実装は意外と簡単です。では早速。

■フレームワーク

StoreKitが必要になります。

#import <StoreKit/StoreKit.h>

■クラスの定義

以下のようにしてSKProductsRequestDelegateとSKPaymentTransactionObserverを実装します。ビューコントローラでなくても大丈夫です。ちなみに今回はローディング中の処理も解説コードに入れてます。

@interface HogeViewController : UIViewController<SKProductsRequestDelegate, SKPaymentTransactionObserver> {
    bool isLoading;
    SKProductsRequest       *skProductsRequest;
    UIActivityIndicatorView *spinner;
    UIView                  *loaderBg;
    UILabel                 *loaderTitle;
}
@property bool isLoading;
- (IBAction)purchaseButtonPushed:(id)sender;
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response;
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransaction:(NSArray *)transactions;
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_3_0);

■実装

購入ボタンが押されたときの処理

ここから購入処理の通信が始まります。UIActivityIndicatorViewを使うと簡単にロード中の表示ができるのですが、背景色によっては全く見えない場合がありますので、1枚UIViewを下に敷いて見やすくしましょう。また、1点注意があるのですがUIによっては購入ボタンの2度押しが可能ですので、フラグを立ててしっかり防ぐようにしましょう。

- (IBAction)purchaseButtonPushed:(id)sender {
    if([[UIDevice currentDevice] networkAvailable] == NO){
        return;
    }
    if(self.isLoading){
        return;
    }
    if([SKPaymentQueue canMakePayments]){
        self.isLoading = true;
       
        // loader
        loaderBg = [[UIView alloc] init];
        [loaderBg setFrame:CGRectMake(100, 150, 120, 85)];
        [loaderBg setBackgroundColor:[UIColor blackColor]];
        [loaderBg.layer setCornerRadius:13.0f];
        [loaderBg setAlpha:0.7];
        [self.view addSubview:loaderBg];
       
        loaderTitle = [[UILabel alloc] init];
        [loaderTitle setTextColor:[UIColor whiteColor]];
        [loaderTitle setFrame:CGRectMake(0, 57, 120, 18)];
        [loaderTitle setText:@"Loading..."];
        [loaderTitle setTextAlignment:UITextAlignmentCenter];
        [loaderTitle setBackgroundColor:[UIColor clearColor]];
        [loaderTitle setAlpha:1.0];
        [loaderBg addSubview:loaderTitle];
       
        // spinner
        spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
        [spinner setCenter:CGPointMake(self.view.frame.size.width/2.0, self.view.frame.size.height/2.0)];
        [self.view addSubview:spinner];
        [spinner startAnimating];
        [UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
       
        // 課金部分
        // identiferを元にappleサーバに問い合わせます
        [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
        skProductsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObject:@"identifer"]];
        [skProductsRequest setDelegate:self];
        [skProductsRequest start];
    }
    else{// 本体の設定でアプリ内課金をOFFにしている人向けの表示
        [self showAlert:@"cannot purchase" text:@"設定 > 一般 > 機能制限で[App内での購入]をONにしてください"];
    }
}

アプリ内課金は本体の設定で無効にできますので、必ず分岐してあげましょう。

appleのサーバにidentiferをお問い合わせした結果の処理

なんてことはありません。identiferをセットしてリクエストしたレスポンスに商品データがあれば、その商品の購入手続きに入ります。

#pragma mark -
#pragma mark SKProductsRequestDelegate
- (void) productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
    if (response == nil) {
        return;
    }
    for(SKProduct *product in response.products){// productを元にした購入オブジェクトをキューに入れて購入手続きに入ります
        SKPayment *payment = [SKPayment paymentWithProduct:product];
        [[SKPaymentQueue defaultQueue] addPayment:payment];
    }
}

ここの部分で何度か購入を繰り返しているとクラッシュしてしまう人もいるかもしれません。それは最後述の方法で回避できます。

購入のトランザクションの状態が変わったときに呼び出される処理

状態はswitch文で分岐しています。完了した時の部分にアプリに合わせた処理を呼び出してあげましょう。

#pragma mark -
#pragma mark SKPaymentTransactionObserver
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransaction:(NSArray *)transactions {
    BOOL isFinished = YES;
    for (SKPaymentTransaction *transaction in transactions) {
        switch (transaction.transactionState) {
            case SKPaymentTransactionStatePurchasing:// 何らかのOKを押す前の処理
                break;
            case SKPaymentTransactionStatePurchased:// success : 決済手続き完了処理
                [queue finishTransaction:transaction];
               
                // もし自社サーバでユーザが購入を完了したかどうかappleサーバに確認する場合は、
                // transaction.transactionReceiptの値をbase64に変換して自社サーバに送信します。
                // [transaction.transactionReceipt base64EncodedString];
               
                isFinished = NO;
                break;
            case SKPaymentTransactionStateFailed://  途中でキャンセルした時
                isFinished = NO;
                [queue finishTransaction:transaction];
                break;
            case SKPaymentTransactionStateRestored:// 通常はコールされない
                isFinished = NO;
                [queue finishTransaction:transaction];
                break;
            default:
                break;
        }
    }
    if (isFinished == NO) {// トランザクションが何らかの完了をした時=>ローディングを消す
        self.isLoading = false;
        [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
        [loaderTitle removeFromSuperview];
        [loaderBg removeFromSuperview];
        [spinner stopAnimating];
    }
}
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_3_0){
    [self paymentQueue:queue updatedTransaction:transactions];
}
サーバ側

ユーザから送信されてきたレシート情報を自社サーバ側でappleに確認するには、以下のようなコードで処理してください。

$ch  = curl_init();
$url = "https://sandbox.itunes.apple.com/verifyReceipt";// サンドボックス(テスト用)
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, TRUE);
curl_setopt($ch, CURLOPT_POSTFIELDS, '{"receipt-data" : "' . $receipt . '"}');
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
curl_setopt($ch, CURLOPT_HEADER, FALSE);
$result = curl_exec($ch);

テスト

xcodeからインストールしたアプリではサンドボックス経由という事が購入ダイアログで表示されます。storeからインストールしたアプリでは本番と同じダイアログになります。特にコードを変更する必要はなく自動で判定してくれるようです。

最後に

だいたいOKなんですが実は複数回購入手続きをするとクラッシュします。dealloc部分で課金処理キューからオブザーバーをしっかり削除するようにしましょう。

- (void)dealloc{
    [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];// これ!!!
    [loaderBg release];
    [loaderTitle release];
    [skProductsRequest release];
    [spinner release];
    [super dealloc];
}

実はここの部分でクラッシュの原因が分からず凄く苦労しました。(,,Ծ‸Ծ,, )