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

2014年9月8日月曜日

Swift環境からObjective-Cで書かれたCocoaPodsを使用する
(SwiftでFMDBを使う)

Swift環境からObjective-Cで書かれたCocoaPodsのライブラリを利用してみる
ライブラリはFMDBを取り込んで、FMDBを使用した簡単なプログラムをSwiftを用いて作成します。
空プロジェクトの作成
まずは、空プロジェクトを作成し、そしてすぐに閉じます。CocoaPodsでFMDBを取り込むプロジェクトファイルだけを前もって作成します。

※因みに本サンプルはXcode6 beta7にて作成しています。
podfileの作成とインストール
podfileの内容を以下のようにして作成したプロジェクトの直下に作成し、
pod 'FMDB'
以下コマンドを実行してインストールを実行。
% pod install
インストールしたら、作成されたワークスペース(プロジェクトファイルじゃないです)を開きます。 下記の様にコマンドラインから起動しても良いですし、FinderからXXX.xcworkspaceファイルをダブルクリックしても良いです。
% open XXX.xcworkspace
ワークスペースを起動したら、以下のようにPodsプロジェクトが作成されていることを確認して下さい。
※CocoaPodsは既にインストール済みを前提とします。CocoaPodsのインストールやそのた詳細についてはCOCOAPODSを参照。

Bridging Headerの作成
Objective-CをSwift環境から利用するにはBridging Headerを作成する必要があります。
このヘッダにSwift環境で使用するObjective-Cのヘッダファイルを記載することになります。
Bridging-Headerは、
1)仮のObjective-Cのファイルを作成することで自動作成、自動設定する方法(自動生成)
2)個別に作成し設定する方法(独自作成)
があります。
Bridging Headerを自動生成する場合は
Objective-Cのファイルを生成します。 Xcode menu > File > New > File... を選択しファイル新規作成します。 以下の様に、Objective-C Fileを選択しNextを押下。
ファイル名は後ほど削除するので、何でも良いです。
すると、以下の様に bridging header を作成するか問われるのでYESを押下します。
以下の様に bridging headerが作成されます。
以上で完了です。後はObjective-Cのヘッダファイルをbridging headerに記載するのみです。 FMDBの場合は以下のようにすれば良いでしょう。
//
//  Use this file to import your target's public headers that you would like to expose to Swift.
//
#import "FMDatabase.h"
作成したdummy.mファイルは削除して構いません。
Bridging Headerを独自に作成、設定する場合は
まずはbridging headerを作成します。 ファイル名はなんでも良いですが、bridging headerと分かるようにしましょう。
作成したら、BuildSettingを変更します。作成したプロジェクトを選択して、Swift Conpiler - Code Generation の設定項目値の Objective-C Bridging Header の項目を編集します。
ヘッダの内容は自動設定の時と同じです。自前でヘッダを作成すると#ifdef/#endifが自動挿入されますが、特に削除して問題無いでしょう。(自動作成時に存在しなかったので)
//  Bridging-Header.h
//  UsingCocoaPodsInSwift
//
#import "FMDatabase.h"

以上で、準備は終わりです。
SwiftからFMDBのメソッド呼び出し
あとはSwiftでコードが書いていくだけです。FMDBが提供しているメソッドの呼び出しはSwift言語仕様に変換されて呼び出すことになりますが、Xcodeのコード補完の機能があるので、メソッド名さえ分かっていればそれほど難しくはないでしょう。
抜粋しますが、FMDBに関するメソッドとして、初期化と更新クエリの呼び出し箇所を載せておきます。詳細は後述するGitHub上のソースコードを参照下さい。

FMDBインスタンス生成
        m_db = FMDatabase(path:m_dbPath)
トランザクションを開始してインサート処理
    func createNewContent(content:String) -> Bool {
        if m_db.goodConnection() {
            m_db.beginTransaction()
            m_db.executeUpdate("insert into memo (contents, 'update') values (?, ?)", withArgumentsInArray: [content, NSDate()])
            m_db.commit()
            //OK
            return true
        }
        //FAIL, database not opend.
        return false;
    }
詳細はGitHubに上げていますので、適宜参照して下さい。
https://github.com/takuran/UsingCocoaPodsInSwift
以下画面サンプル。

2014年4月30日水曜日

UIScrollViewDelegateの実装とスクロールイベントの通知について(iOS)

UIScrollViewクラスにはそのスクロールイベント等を通知するdelegateが定義されており、 プロトコル UIScrollViewDelegate として定義されています。 delegateの各メソッドが呼び出されるタイミングについて、以下にまとめたいと思います。
UIScrollViewDelegateの用途
そもそもUIScrollViewDelegateは何のために使われるのでしょうか?よくUITableViewControllerを利用しているアプリでNavigationBarやToolBarがスクロールに応じて表示/非表示されるものがありますが、そのようにスクロールのイベントに応じて何かしらのアクションを実行したい場合に利用出来ます。
UIScrollViewDelegateプロトコル
UIScrollViewDelegateには大きく
  • スクロール、ドラッグ関連のメソッド
  • ズーム管理に関するメソッド
が定義されていますが、本記事ではスクロール、ドラッグ関連のメソッドについてのみ記載しています。
以下、UIScrollDelegateプロトコルに定義されているメソッドについてスクロール状況に伴う通知タイミングをまとめました。

UIScrollViewDelegate protocol methodの通知タイミング
メソッド名 通知タイミング
- (void)scrollViewDidScroll:(UIScrollView *)scrollView スクロールが発生すると呼び出される。スクロール中は常に呼び出されます。
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView ステータスバーをタップしてViewの先頭にスクロールが発生する前に呼び出される
※本メソッドには戻り値がありYESを返却するとスクロールが発生して、もしNOを返却した場合はスクロールが発生しません。
- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView ステータスバーをタップして先頭にスクロールが完了した後に呼び出される。
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView ドラッグしてスクロールが発生する直前に呼び出される。
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate ドラッグ完了後に呼び出される。慣性スクロールしていてもドラッグを止めた時点で呼び出されます。
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView スクロールが発生して、慣性スクロールの減速が始まる直前に呼び出される。ほぼドラッグ完了後の通知と同じタイミングで通知されます。(こちらの通知が若干遅い)
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView 慣性スクロールの減速が止まった時。つまりスクロールが停止した時に呼び出されます。
タイミングが分かり難いので、以下に時系列で通知タイミングを表した図を書いてみました。
UIScrollViewDelegateの設定
TableViewの場合はUITableViewDelegateがUIScrollViewDelegateを採用しているので、UITableVIewDelegateを設定することで同時にUIScrollviewDelegateを設定することになります。
以下UITableViewControllerの実装クラスでdelegateを設定する場合の例です。UITableViewのdelegateに設定します。
    //scrollview delegate
    self.tableView.delegate = (id)delegate;
コードの詳細は以下のサンプルアプリのソースコードをGitHubにあげているので、そちらを参照して下さい。
UIScrollViewDelegateの通知タイミングを知らせるサンプル・アプリケーション
UIScrollViewDelegateを採用して通知タイミングを知らせるサンプルアプリを作りました。各メソッドが呼び出されると呼び出されたタイミングでインジケータが光るようなアプリです。
微妙な通知タイミングがこのアプリで分かると思います。(スクリーンショットじゃ分からないと思いますので、ダウンロードしてお試し下さい)
以下スクリーンショット。

以下にソースコードを公開しているので、コード等参考にして下さい。
UIScrollViewDelegateIndicator on GitHub.

2014年3月11日火曜日

Objective-C リテラル構文備忘録(Objective-C Literals)

Objective-Cリテラル構文備忘録
NSNnumber
特筆することは無いが、@(expression)には注意。
    //NSNumber literals
    
    //bool
    //NSNumber *boolYesValue = [NSNumber numberWithBool:YES];
    NSNumber *boolYesValue = @YES;
    NSLog(@"%@", boolYesValue); // 1
    //NSNumber *boolNoValue = [NSNumber numberWithBool:NO];
    NSNumber *boolNoValue = @NO;
    NSLog(@"%@", boolNoValue); // 0
    
    //char
    //NSNumber *charValue = [NSNumber numberWithChar:'Z'];
    NSNumber *charValue = @'Z';
    NSLog(@"%c", [charValue charValue]); // Z
    
    //double
    //NSNumber *doubleValue = [NSNumber numberWithDouble:3.14];
    NSNumber *doubleValue = @3.14;
    NSLog(@"%f", [doubleValue doubleValue]); //3.140000
    
    //float
    //NSNumber *floatValue = [NSNumber numberWithFloat:3.14F];
    NSNumber *floatValue = @3.14F;
    NSLog(@"%f", [floatValue floatValue]); //3.140000
    
    //signed int
    //NSNumber *intValue = [NSNumber numberWithInt:INT_MAX];
    NSNumber *intValue = @INT_MAX;
    NSLog(@"%d", [intValue intValue]); // 2147483647
    
    //unsigned int
    //NSNumber *uintValue = [NSNumber numberWithUnsignedInt:UINT32_MAX];
    NSNumber *uintValue = @UINT32_MAX;
    NSLog(@"%u", [uintValue unsignedIntValue]); // 4294967295
    
    //signed long
    //NSNumber *longValue = [NSNumber numberWithLong:LONG_MAX];
    NSNumber *longValue = @LONG_MAX;
    NSLog(@"%ld", [longValue longValue]); // 2147483647
    
    //unsigned long
    //NSNumber *ulongValue = [NSNumber numberWithUnsignedLong:LONG_MAX];
    NSNumber *ulongValue = @LONG_MAX;
    NSLog(@"%ld", [ulongValue unsignedLongValue]); // 2147483647
    
    //long long
    //NSNumber *longlongValue = [NSNumber numberWithLongLong:LONG_LONG_MAX];
    NSNumber *longlongValue = @LONG_LONG_MAX;
    NSLog(@"%lld", [longlongValue longLongValue]); // 9223372036854775807
    
    //expressions
    //NSNumber *expValue = [NSNumber numberWithInt:INT_MAX-1];
    NSNumber *expValue = @(INT_MAX-1);
    NSLog(@"%d", [expValue intValue]); // 2147483646
NSArray
配列のリテラル構文の場合、nilを指定することは出来ないことに注意。 また、リテラル構文で作成されるオブジェクトはimmutableなインスタンスなので、 値を変更したい場合はmutableCopyしてNSMutableArrayを取得する。
    //Array literals
    
    //NSArray *array = [NSArray arrayWithObjects:@1, @2, @3, nil];
    NSArray *array = @[@1, @2, @3];
    NSLog(@"%@", array); // (1, 2, 3)
    
    //添え字アクセス
    //NSNumber *number = [array objectAtIndex:0];
    NSNumber *number = array[0];
    NSLog(@"%@", number); // 1
    
    //添え字による値設定
    NSMutableArray *mutableArray = [@[@1, @2, @3] mutableCopy];
    mutableArray[0] = @10;
    NSLog(@"%@", mutableArray[0]); // 10
NSDictionary
リテラル構文の場合はインスタンスメソッドの順とは異なり、キー、値の順で指定することに注意。 mutableCopyの件は配列の時と一緒。

    //Dictionary literals
    
    //NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:@1, @"one", @2, @"two", @3, @"three", nil];
    NSDictionary *dictionary = @{@"one" : @1,
                                 @"two" : @2,
                                 @"three" : @3};
    NSLog(@"%@", dictionary); // {one = 1; two = 2; three = 3;}
    
    //添え字によるアクセス
    NSNumber *numberValue = dictionary[@"one"];
    NSLog(@"%@", numberValue);
    
    //添え字による値設定
    NSMutableDictionary *mutableDictionary = [@{@"one" : @1, @"two" : @2, @"three" : @3} mutableCopy];
    mutableDictionary[@"one"] = @1.1;
    NSLog(@"%@", mutableDictionary[@"one"]); // 1.1
NSString
Cスタイルの型をNSStringへ変換することが@(expression)表現で可能になっている事に注意。
    //String literals
    NSString *stringValue = @"string value";
    NSLog(@"%@", stringValue);
    
    //CスタイルからのNSString生成
    static const char * const C_CONST_STRING = "CONST STRING";
    //NSString *c_string = [NSString stringWithCString:C_CONST_STRING encoding:NSUTF8StringEncoding];
    //NSString *c_string = [NSString stringWithUTF8String:C_CONST_STRING];
    NSString *c_string = @(C_CONST_STRING);
    NSLog(@"%@", c_string);

2014年2月27日木曜日

iOSでOAuth2認証を行う(feedlyクライアントの作成)

The feedly Cloud APIが一般公開されていることはご存知の方は多いと思うが、APIを利用している方は そう多く無いのではないだろうか。 The feedly Cloud APIはsandboxという形式で開発者に開発用環境を提供している。 開発の序盤はこの開発環境を使用してAPIの使用方法やアプリの動作確認を行い、 その後リリースする準備が出来たら本環境で利用可能なクライアントIDを払い出してもらう という流れになると思う。(本環境に対応したクライアントIDの払い出しは別途Emailで申請する必要があるようだ)
iOSでのOAuth2認証
The feedly Cloud APIを使用するにはまずOAuth2認証が必要だ。これが一番厄介で(昔よりは良くなっている) 認証さえ済ませてしまえば、後はAPIを呼び出して必要な情報を取り出し、好きなように処理すれば良い。 iOS用のOAuth2ライブラリが存在するので、そのライブラリを利用してOAuth2認証を実装してみようと思う。
今回の目標;The Feedly Cloud(Sandbox)にOAuth2認証して認証アカウント一覧を表示する。
FeedlyにOAuth2認証して、プロファイル取得APIを呼び出してアカウントプロファイル情報の取得を行う。 認証したユーザを一覧表示する機能と認証済みアカウントの削除(サインアウト)を行う機能を実装する。
OAuth2ライブラリ
さてさて、iOSで利用可能なOAuth2ライブラリはいくつか存在する。以下2つのライブラリを試してみた。 gtm-oauth2はGoogleが提供しているライブラリで、少し試してみたが、gtm-oauth2では 画面(UIViewController)が提供されており、UIWebViewの実装も提供されている。 やや、Google認証に特化したコードやUIがiOS7対応されておらず結局自分で修正する必要があった。 一方、OAuth2Clientは画面の提供までは無いか、その分汎用性が高く 自前でUIWebViewを実装して組み込む事が可能な作りになっている。 また、認証データはKeychainに格納され、アクセストークンが無効になった場合はリフレッシュトークンから アクセストークンの再取得も行ってくれると思う。 ということで、今回はOAuth2Clientを利用させてもらい、OAuth2認証 を試してみたい。
Feedly Cloud Developer Programへのサインアップ(現在は必要無し)
昔はThe feedly Clound APIを利用するために、サインアップ用フォームに必要事項を入力し、 clientId, clientSecretを払いだしてもらっていたが、最近は 開発者用developer forum に提示されるように変更されているみたいだ。面倒くさくなったのかな。 因みに、払いだしたclientId, clientSecretには有効期限があり毎月1日にリセットされてしまう。
OAuth2Clientライブラリの取り込み
取り込みはCocoaPodsで一発。本当に便利だなぁ。もうCocoaPods無しの生活には戻れないほど便利。 本家のGitHubからCocoaPodsで取り込めば良いと思うが、後述する理由により改善したバージョンを GitHubに上げているので、用途に応じてどちらを取り込むか決めて欲しい。 CocoaPodsでの取り込み方法も含め、後述する。
OAuth2認証
OAuth2の認証フローについて整理すると以下のイメージになる。と思う。 このイメージにそって説明していきたいと思う。
認証ページを表示するUIWebViewの作成
StoryBoardでアカウント一覧を表示するUIViewControllerと認証Webページを表示するUIViewControllerを作成する。 以下のイメージで作成しているが、認証用のWebViewを配置していれば他は重要ではないので、任意で作成してもらって構わない。
OAuth2Clientの初期化と初期設定
初期化するのにベストな箇所はOAuth2ClientのREADMEにも書かれている通り次の箇所がベストだろう。
+[UIApplicationDelegate initialize]
ん?ってどこやねん。って思ったが、AppDelegate.mのこと。AppDelegateはUIApplicationDelegateプロトコル を採用しているクラスなので、この箇所はアプリケーション起動時に初期化されることになると思う。 以下サンプルコードのクライアントシークレットは実際のものに差し替えて欲しい。 また、ClientIdとRedirectUrlはFeedly Cloud Ssndbox用のものになる。本来は公開するものではないが、 開発用で一般に公開されているものなので、特に伏せ字にはしていない。
NTAppDelegate.mの一部抜粋。
#import "NXOAuth2.h"

//for Feedly Oauth2(sandbox)
//account type
static NSString * const kOauth2ClientAccountType = @"Feedly";
//clientId
static NSString * const kOauth2ClientClientId = @"sandbox";
//Client Secret
static NSString * const kOauth2ClientClientSecret = @"CLIENTSECRET";
//Redirect Url
static NSString * const kOauth2ClientRedirectUrl = @"http://localhost";
//base url
static NSString * const kOauth2ClientBaseUrl = @"https://sandbox.feedly.com";
//auth url
static NSString * const kOauth2ClientAuthUrl = @"/v3/auth/auth";
//token url
static NSString * const kOauth2ClientTokenUrl = @"/v3/auth/token";
//scope url
static NSString * const kOauth2ClientScopeUrl = @"https://cloud.feedly.com/subscriptions";

+ (void)initialize {
    NSString *authUrl = [kOauth2ClientBaseUrl stringByAppendingString:kOauth2ClientAuthUrl];
    NSString *tokenUrl = [kOauth2ClientBaseUrl stringByAppendingString:kOauth2ClientTokenUrl];

    //setup oauth2client
    [[NXOAuth2AccountStore sharedStore] setClientID:kOauth2ClientClientId
                                             secret:kOauth2ClientClientSecret
                                              scope:[NSSet setWithObjects:kOauth2ClientScopeUrl, nil]
                                   authorizationURL:[NSURL URLWithString:authUrl]
                                           tokenURL:[NSURL URLWithString:tokenUrl]
                                        redirectURL:[NSURL URLWithString:kOauth2ClientRedirectUrl]
                                     forAccountType:kOauth2ClientAccountType];
}
次に、UIWebViewを設定したUIViewControllerにて認証の成功可否を通知してもらうための設定を行う。
NTAuthWebViewController.mの一部抜粋
定義してるヘッダとUIWebViewのアウトレット。
#import "NTAuthWebViewController.h"
#import "NXOAuth2.h"
#import "NTAppDelegate.h"

@interface NTAuthWebViewController ()
@property (weak, nonatomic) IBOutlet UIWebView *webView;
@property (strong, nonatomic) id successObserver;
@property (strong, nonatomic) id failObserver;
認証用の通知設定。それぞれ認証成功、失敗時に通知されるオブザーバーを登録している。 WebViewを閉じたら通知の必要はなくなるので、解除用にオブザーバーオブジェクトを退避している。 認証成功時はユーザプロファイル取得用のメソッドを呼び出している。OAuth2ClientのAccount情報にはユーザデータを 格納する領域が用意されており、ユーザプロファイル等のデータを格納してKeychainに保存することが出来る。 フロー図の(4)まで完了すれば、成功の通知が行われる。
- (void)p_addOauth2Notification {
    //setup notifications for success or fail
    //for success
    self.successObserver = [[NSNotificationCenter defaultCenter]
                            addObserverForName:NXOAuth2AccountStoreAccountsDidChangeNotification
                            object:[NXOAuth2AccountStore sharedStore]
                            queue:nil usingBlock:^(NSNotification *notification) {
                                NSLog(@"Success.");
                                
                                //get authinticate userinfo
                                NSDictionary *dict = notification.userInfo;
                                NXOAuth2Account *account = [dict valueForKey:NXOAuth2AccountStoreNewAccountUserInfoKey];
                                //get user profile
                                [self p_getUserProfile:account];
                                
                            }];
    
    //for fail
    self.failObserver = [[NSNotificationCenter defaultCenter]
                         addObserverForName:NXOAuth2AccountStoreDidFailToRequestAccessNotification
                         object:[NXOAuth2AccountStore sharedStore]
                         queue:nil
                         usingBlock:^(NSNotification *note) {
                             NSLog(@"Fail.");
                             
                             //pop navigation controller
                             [self.navigationController popViewControllerAnimated:YES];
                             
                         }];

}
フローの(1)の開始。認証を開始する。OAuth2Clientの方で準備されたNSURLが受け渡されるので、そのNSURLを使用して UIWebViewでWebアクセスを行う。 フローの(2)へ遷移し、認証用ページが表示されるので、ユーザ、パスワードを入力する。後はライブラリ側で フローの(3)、(4)を処理してくれて、成功した場合は通知が行われることになる。
- (void)p_startRequest {
    [[NXOAuth2AccountStore sharedStore] requestAccessToAccountWithType:kOauth2ClientAccountType
                                   withPreparedAuthorizationURLHandler:^(NSURL *preparedURL) {
                                       //start authentication request.
                                       [_webView loadRequest:[NSURLRequest requestWithURL:preparedURL]];
                                       
                                   }];
}
こんな感じで上記メソッドを呼び出している。
- (void)viewDidLoad
{
    [super viewDidLoad];
 // Do any additional setup after loading the view.

    _webView.delegate = self;
    
    //init notifications
    [self p_addOauth2Notification];
    [self p_startRequest];
    
}
ユーザプロファイル取得のコードと通知用オブザーバーの削除は以下。
- (void)p_getUserProfile:(NXOAuth2Account *)account {
    //get user profile on feedly user
    NSLog(@"account info : %@", account);
    
    NSURL *targetUrl = [NSURL URLWithString:@"https://sandbox.feedly.com/v3/profile"];
    [NXOAuth2Request performMethod:@"GET"
                        onResource:targetUrl
                   usingParameters:nil
                       withAccount:account
               sendProgressHandler:^(unsigned long long bytesSend, unsigned long long bytesTotal) {
                   //TODO
               }
                   responseHandler:^(NSURLResponse *response, NSData *responseData, NSError *error) {
                       NSLog(@"error : %@", error);
                       NSLog(@"response : %@", response);
                       NSString *jsonString = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
                       NSLog(@"response data : %@", jsonString);
                       
                       //
                       if (!error) {
                           //success
                           NSLog(@"get profile success.");
                           //json変換してDictionary型をuserDataとして格納する
                           NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:responseData options:kNilOptions error:nil];
                           if (dict) {
                               //set user data
                               [account setUserData:dict];
                           }
                           
                           //pop viewcontroller
                           [self.navigationController popViewControllerAnimated:YES];
                           
                       } else {
                           //error
                           NSLog(@"get profile failer.");
                       }
                       
                   }];
    
}

- (void)viewDidDisappear:(BOOL)animated {
    //hide network activity indicator
    [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
    //remove notifications
    [self p_removeOauth2Notification];
    
}

- (void) p_removeOauth2Notification {
    [[NSNotificationCenter defaultCenter] removeObserver:self.successObserver];
    [[NSNotificationCenter defaultCenter] removeObserver:self.failObserver];
}
自前のUIWebViewを利用しているので、フローの(3)リダイレクトのハンドリングは 自前コードで記載してライブラリ側へ受け渡す必要がある。 以下メソッドはUIWebVIewがHTTPリクエストを行う前に呼び出されるメソッドで、 このメソッドでリクエスト先URLをハンドリングしてリダイレクトURLと等しいか判定 する必要がある。リダイレクトURLと等しいかの判定処理はライブラリ側で行ってくれる。 その呼出が、
if ([[NXOAuth2AccountStore sharedStore] handleRedirectURL:[request URL]]) {
となる。
#pragma mark - UIWebViewDelegate

- (BOOL) webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    if ([[NXOAuth2AccountStore sharedStore] handleRedirectURL:[request URL]]) {
        return NO;
    }
    return YES;
}
サンプルコードはGitHubにあげてあるので、省略した部分やアカウント一覧を表示している部分等は適宜GitHubの方を 参照して欲しい。 以下、実行画面。
The Feedly Cloud(sandbox)利用時のOAuth2Clientライブラリの問題
実は本家OAuth2Clientのままだと認証に成功することはなかった。 その原因はリダイレクトURLにあった。 Feedly(sandbox)の場合はリダイレクトURLが基本的に
http://localhost
となる。リダイレクトはフローの(3)でリダイレクトURLに認可コードがURLパラメータに付与されて通知されるのだが、
http://localhost/?code=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
のように通知される。このURLをリダイレクトしているのだが、「?」よりも前をリダイレクトURLとして 扱ってしまっている。
http://localhost/?code=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
これは間違っていないと思うが、リダイレクトURLがルートパスで終わってしまっている場合、http://localhostの後に「/」が サーバ側で付与されてしまい、結果的に設定しているリダイレクトURLと異なることとなり、リダイレクトのハンドリング に失敗してしまう。
http://localhost/
そこで、リダイレクトURLは初期化で設定してある値を受け渡すように修正した。 Feedly(sandbox)ではリダイレクトURLが固定(http://loaclhost以外にも定義されてはいるが)のため本改造を行った。 リダイレクトURLがルートパス以外であればこのような問題は発生していないと考えられる。
OAuth2Clientのインストール
CocoaPodsで。 上記で述べたリダイレクトURLの問題改善版を入れたい場合は、私が本家をForkして改善したものをGitHubに 登録してあるので、以下のようにPodfileを記載すればよい。
platform :ios, '6.0'
pod 'NXOAuth2Client', :git => 'https://github.com/takuran/OAuth2Client.git'
なんか怪しいので、本家をいれて試してみたいという人は以下のようにPodfileを記載すればよい。
platform :ios, '5.0'
pod 'NXOAuth2Client', '~> 1.2.5'
Podfileを作成したら以下コマンドでインストール完了。
#事前にpodのインストールはする必要あり。以下参照。
http://cocoapods.org/
% pod install

2012年5月8日火曜日

iOSでSQLiteを使う2(FMDBのFMDatabaseQueueクラスを使ってみる)

FMDBにはスレッド間でSQLiteを使用する際のヘルパークラスが提供されています。そのクラスがFMDatabaseQueueです。 このクラスの用途と、どういったシーンで使用していけば良いのかを考えつつ、サンプルプログラムで試してみます。

因みに、FMDBの基本的な使い方に関しては前回書いた iOSでSQLiteを使う(FMDB) を参照。
マルチスレッドにおけるFMDatabaseインスタンスに関して

FMDBのREADMEにはFMDatabaseのインスタンスをシングルトンにして、マルチスレッド上から唯一のFMDatabaseインスタンスを 取得して使用することは推奨されておらず、一見うまく動いているようでも、いつかはクラッシュしたり、例外が 発生することになるだろうと書かれています。つまり、FMDatabaseクラスはスレッドセーフでは無いということになります。

ちなみに、シングルトンじゃなくてスレッド毎にFMDatabaseインスタンスを生成すれば良いんじゃ?という考え方もあるかもしれない。 確かにそのアプローチもアリかと思いますが、FMDBのトランザクションが読み取り・書き込み排他ロックをデフォルトに してるっぽいので、複数インスタンスを生成してもDBへのアクセスは結局排他されるのであまり意味が無く、余計にメモリ を使用してしまうだけだと思う。(beginTransactionでトランザクションを開始した場合)
beginDeferredTransactionにてトランザクションを開始した場合は、読み取りは共有ロックになるので、 更新中の読み取りが可能になりますが、そもそもSQLiteへのアクセスは自身のアプリケーションからのみであるため そうそう更新中の読み取りを実施したいという要望は無いはず。それより、SQLiteへのアクセスは直列化してアクセスさせ、 処理をシンプルにする方がメリットが大きいのではないでしょうか。

試しにマルチスレッド上で同一のFMDatabaseインスタンスを使ってみる

以下のコードは、DBを検索してその結果を文字列としてUITextView上に表示するという処理を 10個の新たなスレッド上で実行させています。 ちなみに、以下コードのDB検索部分を載せていませんが 前回の記事 と同じものです。

    //結果取得して画面描画を別スレッドで行う
    //スレッドはNSOperationQueueにて生成
    NSOperationQueue *opeQueue = [[NSOperationQueue alloc]init];

    //同期用のオブジェクト(インデックス表示用)
    NSObject *sync = [[NSObject alloc]init];
    static int index = 0;

    //スレッド実行部分のブロック処理
    void (^exec)(void)  = ^(void) {
        int _index = 0;
        @synchronized(sync) {
            index++;
            NSLog(@"新しいスレッド。index : %d.", index);
            _index = index;
        }
        
        //DB検索を実行
        NSArray *result = [db executeQuery2];
        //検索結果の文字列を作成
        NSString *text = [NSString stringWithFormat:@"%d\n", _index];
        for (ExampleRecord *record in result) {
            text = [text stringByAppendingString:
                    [NSString stringWithFormat:@"%d, %@, %@, %f\n",
                        record.columnOfInteger,
                        record.columnOfDate,
                        record.columnOfText,
                        record.columnOfReal]];
        }
        
        //特に意味ないけど、2秒スリープ
        [NSThread sleepForTimeInterval: 2.0];
        //画面に表示。メインスレッド以外からの更新なのでperformSelectorOnMainThreadを使用。
        if ([result count] != 0) {
            [self performSelectorOnMainThread:@selector(updateTextView:) withObject:text waitUntilDone:NO];
        }

    };//end of the block.
    
    //新しいスレッドを生成し実行
    [opeQueue addOperationWithBlock: exec];
    [opeQueue addOperationWithBlock: exec];
    [opeQueue addOperationWithBlock: exec];
    [opeQueue addOperationWithBlock: exec];
    [opeQueue addOperationWithBlock: exec];
    [opeQueue addOperationWithBlock: exec];
    [opeQueue addOperationWithBlock: exec];
    [opeQueue addOperationWithBlock: exec];
    [opeQueue addOperationWithBlock: exec];
    [opeQueue addOperationWithBlock: exec];

上記処理を何度も実行すると以下のような例外が発生します。以下は例外時のイメージですが、 結構何度も実行させてやっと発生した感じです。通常は例外が発生しないでFMDBの各メソッドがエラー返却される パターンが殆どでした。まあ、エラーになることはこれで分かりました。


・デバッガ

・標準出力結果(backtrace)
FMDatabaseQueueを使用して実行してみる

マルチスレッド上からFMDatabaseQueueを使用してDB検索を実行してみます。

    //結果取得して画面描画を別スレッドで行う
    //スレッドはNSOperationQueueにて生成
    NSOperationQueue *opeQueue = [[NSOperationQueue alloc]init];

    //同期用のオブジェクト(インデックス表示用)
    NSObject *sync = [[NSObject alloc]init];
    static int index = 0;

    //スレッドで実行するブロック定義
    void (^exec)(void) = ^(void) {
        int _index=0;
        //各スレッドで表示するインデックスインクリメント
        //ここはログ表示用なのであまり気にしないで下さい。
        @synchronized(sync) {
            index++;
            _index = index;
        }
        NSLog(@"新しいスレッド。index : %d.", _index);        
        
        //SQLを実行。以下のメソッド内でFMDatabaseQueueクラスを使用しています。
        [db executeQuery3:self index:_index];
    };
    
    //スレッド実行
    [opeQueue addOperationWithBlock:exec];
    [opeQueue addOperationWithBlock:exec];
    [opeQueue addOperationWithBlock:exec];
    [opeQueue addOperationWithBlock:exec];
    [opeQueue addOperationWithBlock:exec];
    [opeQueue addOperationWithBlock:exec];
    [opeQueue addOperationWithBlock:exec];
    [opeQueue addOperationWithBlock:exec];
    [opeQueue addOperationWithBlock:exec];
    [opeQueue addOperationWithBlock:exec];

実際にDBアクセス部分(上記のexecuteQuery3というメソッド)のコードは以下になります。 以下コードで、_dbQueueという変数がFMDatabaseQueueのインスタンスです。インスタンス生成部分のコードは 別途示します。

- (void)executeQuery3:(id)delegate index:(int)index {
    if (_dbQueue) {

        //トランザクション無しのパターン
        //_dbQueue変数はFMDatabaseQueueのインスタンスです。生成は別途実施済みです。
        [_dbQueue inDatabase:^(FMDatabase *db) {
            FMResultSet* rs = [db executeQuery:@"select * from example where t like ?", @"%2."];
            
            if ([db hadError]) {
                NSLog(@"Err %d: %@",
                      [db lastErrorCode], [db lastErrorMessage]);
            }

            NSString *text = [NSString stringWithFormat:@"%d\n", index];
            while ([rs next]) {
                text = [text stringByAppendingString:
                        [NSString stringWithFormat:@"%d, %@, %@, %f\n",
                                [rs intForColumnIndex:0],
                                [rs dateForColumnIndex:1],
                                [rs stringForColumnIndex:2],
                                [rs doubleForColumnIndex:3]]];
            }

            //sleep
            [NSThread sleepForTimeInterval:2.0];
            
            //update UI.
            [delegate performSelectorOnMainThread:@selector(updateTextView:) withObject:text waitUntilDone:NO];
        }];
        
        NSLog(@"call finished. index : %d.", index);
    }
}

このコードで何度か実行しましたが、例外やFMDBのクラスからエラーが発生することはありませんでした。 実行すると処理内で2秒スリープしているため2秒間隔で画面が更新されていきます(以下、赤字部分)。

・標準出力の結果
2012-05-08 06:52:00.515 FMDBTest[15557:f803] ViewDidLoad was finished.
2012-05-08 06:52:00.569 FMDBTest[15557:12e03] 新しいスレッド。index : 1.
2012-05-08 06:52:00.570 FMDBTest[15557:12f03] 新しいスレッド。index : 2.
2012-05-08 06:52:00.570 FMDBTest[15557:11103] 新しいスレッド。index : 3.
2012-05-08 06:52:00.571 FMDBTest[15557:13003] 新しいスレッド。index : 4.
2012-05-08 06:52:00.576 FMDBTest[15557:13203] 新しいスレッド。index : 5.
2012-05-08 06:52:00.580 FMDBTest[15557:13c03] 新しいスレッド。index : 6.
2012-05-08 06:52:00.588 FMDBTest[15557:14403] 新しいスレッド。index : 8.
2012-05-08 06:52:00.596 FMDBTest[15557:1450b] 新しいスレッド。index : 10.
2012-05-08 06:52:00.587 FMDBTest[15557:14203] 新しいスレッド。index : 7.
2012-05-08 06:52:00.595 FMDBTest[15557:14303] 新しいスレッド。index : 9.
2012-05-08 06:52:02.585 FMDBTest[15557:12e03] call finished. index : 1.
2012-05-08 06:52:04.588 FMDBTest[15557:12f03] call finished. index : 2.
2012-05-08 06:52:06.590 FMDBTest[15557:11103] call finished. index : 3.
2012-05-08 06:52:08.591 FMDBTest[15557:13003] call finished. index : 4.
2012-05-08 06:52:10.593 FMDBTest[15557:13203] call finished. index : 5.
2012-05-08 06:52:12.594 FMDBTest[15557:13c03] call finished. index : 6.
2012-05-08 06:52:14.596 FMDBTest[15557:14403] call finished. index : 8.
2012-05-08 06:52:16.598 FMDBTest[15557:1450b] call finished. index : 10.
2012-05-08 06:52:18.600 FMDBTest[15557:14203] call finished. index : 7.
2012-05-08 06:52:20.602 FMDBTest[15557:14303] call finished. index : 9.


・シュミレーター画面

上のコードをちょっと解説します。

・FMDatabaseQueueの生成部分
上記のサンプルコードには表れていませんが、FMDatabaseQueueのインスタンス生成はFMDatabaseの生成時と同じでDBパスを引数に指定します。
        _dbQueue = [FMDatabaseQueue databaseQueueWithPath:dbPath];
・FMDatabaseQueueへの処理依頼
処理はブロックで記載する必要があります。ここでいきなりブロックとは?となるかもしれませんが、、 すみません、ここではブロックの解説はしません。 FMDatabaseQueueは主に以下の2つのメソッドを提供しています。
  • - (void)inDatabase:(void (^)(FMDatabase *db))block
  • - (void)inTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block;
トランザクションを必要としない場合(inDatabase)とする場合(inTransaction)です。 トランザクションを必要とする場合はロールバック有無を表す引数が存在します。 ちなみにこのrollbackの変数はC言語のポインタ型なので、値を設定する際は実体に設定する必要がありますので、 注意して下さい。

以下にFMDBのREADMEに載っているサンプルコードを示します。実体に値を設定するには *(アスタリスク)が必要です。#C/C++をご存知のかたは特に注意する必要もないですが
    [queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
                [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]];
                [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:2]];
                [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:3]];

                if (whoopsSomethingWrongHappened) {
                    //
                    //↓ここです。実体に設定するには*が必要です。
                    *rollback = YES;
                      return;
                }
                // etc…
                [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:4]];
    }];
FMDatabaseQueueクラスの機能

FMDatabaseQueue.mのソースを見れば分かりますが、このクラスは次の機能を提供します。

・直列ディスパッチキューを生成
Grand Central Dispatch(GCD)の直列ディスパッチキューを生成します。この直列キューはタスクを同時には 1つしか実行しないので、キューに登録した処理は必ず同期処理されます。つまり、FMDatabaseQueueは全てのSQLite へのアクセスを完全同期していることになります。

・同期型で実行
直列ディスパッチキューからの実行は同期型(dispatch_sync)で実行しているので、呼び出し元スレッドは ブロックされるので注意する必要があります。私はこの部分を勘違いしており、ブロックで指定したタスクは FMDBのQueue側で非同期に実行してくれるものだと勘違いしていました。つまり、重そうなSQLをFMDatabaseQueue にまかせれば後は勝手に非同期でやってくれる訳では無いということです。スレッドは自分で作成する必要があります。


2012年4月25日水曜日

iOSでSQLiteを使う(FMDB)

SQLiteのライブラリはC言語のライブラリとして提供されています。したがって、SQLiteの各関数の呼び出しや引数の指定方法、パラメータの型はC言語の文法・型に従う必要があります。

特にC言語への型変換に関して、DBの場合は数値(整数、浮動小数点数)、文字列、バイナリ、日付等様々な型があり、Cocoa TouchからC言語への型変換をSQL実行時と結果取得時に行う必要があるため、結構面倒だったりします。

そんなこともあってか、iOSのSQLiteラッパーライブラリとしてFMDBというライブラリがあり、このライブラリはこの面倒な型変換を全て受け持ってくれるため、この点だけでもFMDBを使う魅力は大いにあると思います。(注:後述しますが、FMDBは型変換だけでは無いです)
また、FMDBはARC有効/無効どちらにも対応しています。プリプロセッサでARCの有効状態を判定しているみたいですね。

素でいくかFMDBを利用するかの方針決定
 私は素でSQLiteを使う方法とFMDBを使う方法を試しましたが、やはりFMDBを使ったほうがコードが簡単になりますし、なによりもCocoa Touchのクラスを直接使うことが可能である点とマルチスレッド用のクラスやDBプーリングのクラスも提供されていることからFMDBを素直に使用したほうが結局のところ効率が良いと感じました。
ちなみにFMDBのライセンスはMITライセンスです。
以降はFMDBを使用することを前提としています。
FMDBのダウンロード
=========== 2014/3/31 追記 ===================
FMDBのインストールについて追記します。
以下、本記事に沿ってインストールしても問題はありませんが、CocoaPodsでインストールされることをオススメします。 OS Xはデフォルトでrubyがインストールされているので、以下コマンドですぐに利用できます。
・CocoaPodsのインストール
$ sudo gem install cocoapods
・FMDBのインストール
//FMDBをインストールしたいあなた自身が作成したプロジェクト配下へ移動します
$ cd [インストールしたいあなたのプロジェクト]
$ vi Podfile
Podfileを作成する。中身は以下。最新版をインストールするなら以下で良いでしょう。
pod 'FMDB'
・インストール実行
$ pod install
xcodeでワークスペースとして起動する。
$open YourApp.xcworkspace
するか Finderで拡張子がxcworkspaceとなっている方を起動すると 既にFMDBがインストールされた状態となっているはずです。後はヘッダファイルを引き込めばOKですね。 因みに、CocoaPodsでインストールした場合は本記事の「FMDBのダウンロード」と「SQLiteのライブラリ取り込みとFMDBラッパークラスファイルの取り込み」の章はスキップして下さい。
Reference : CocoaPods.
=============== 追記終わり ==================
 FMDBはライブラリと言いましたが、ソースコードとして提供されているので、ダウンロードして必要なファイルをプロジェクトに追加します。
 ソースはgithubで公開されています。https://github.com/ccgus/fmdbブラウザでここを開いて直接ZIPファイルをダウンロードしても良いですし、Xcodeをインストールしているのであれば恐らくgitコマンドがインストールされていると思われるので以下の様にコマンドでgithubのFMDBプロジェクトのcloneをローカルに作成しても良いと思います。

 gitを使う場合コマンドプロンプトを起動して、以下を実行します。実行カレントディレクトリ配下にfmdbのディレクトリが作成されます。
%git clone https://github.com/ccgus/fmdb.git
 Xcode用のプロジェクトファイルもあるのでそのままXcodeで起動してコンパイル確認してみても良いですね。またテストコード用のソースコード(fmdb.m)も付属されており、このソースはFMDBを利用するに大変参考になります。
SQLiteのライブラリ取り込みとFMDBラッパークラスファイルの取り込み

・SQLiteのライブラリを追加

・FMDBのソースファイルをプロジェクトに追加
FMDB関連ファイルを格納するGroupフォルダを作成して格納した方がスッキリする。
DBファイルの事前作成とDBファイルのプロジェクト取り込み
 SQLiteのDBファイルは事前に作成して良いし、アプリケーション上で作成しても良いです。今回は事前に作成しています。
コマンドプロンプトで以下を実行し、初期スキーマを生成しテーブルを一つ作成します。
MacBook-Pro:tmp naoyuki$ sqlite3 sample.db 
SQLite version 3.7.7 2011-06-25 16:35:41Enter ".help" for instructionsEnter SQL statements terminated with a ";"
sqlite>create table example (i integer, n numeric, t text, r real);
sqlite> .schema
CREATE TABLE example (i integer, n numeric, t text, r real);
sqlite> .quit
MacBook-Pro:tmp naoyuki$ 

スキーマはこんな感じです。
CREATE TABLE example (
        i integer,
        n numeric,
        t text,
        r real
);
 前述で作成したDBファイル(sample.db)をプロジェクトに取り込みます。プロジェクトへの取り込みはメニューの「File」-「Add Files to XXX」で表示されるダイアログから該当ファイルを選択してプロジェクトへ取り込みを行います。
初回起動時はDBファイルをコピーしてからDBオープン
 ここまで長くなりましたがやっとここからコードを書きます。
まず初回時はDBスキーマが格納されているDBファイルを/Documents/配下へコピーする必要があります。このへんの詳細は、 Resource Bundle/リソースバンドルiOS 標準ディレクトリパスの取得あたりを参照してもらえると理解出来るとおもいます。
 以下、サンプルコードです。Document配下にDBファイルが無い場合はアプリケーションディレクトリからDBファイルをコピーしてからDBをオープンしています。
//DBファイル名
static NSString* const DB_FILE = @"sample.db";

@implementation SqliteExecute {
    FMDatabase*     _db;
}

- (BOOL)openDatabase {
    //DBファイルへのパスを取得
    //パスは~/Documents/配下に格納される。
    NSString *dbPath = nil;
    NSArray *documentsPath = NSSearchPathForDirectoriesInDomains
                                (NSDocumentDirectory, NSUserDomainMask, YES);
    //取得データ数を確認
    if ([documentsPath count] >= 1) {
        //固定で0番目を取得でOK
        dbPath = [documentsPath objectAtIndex:0];
        //パスの最後にファイル名をアペンドし、DBファイルへのフルパスを生成。
        dbPath = [dbPath stringByAppendingPathComponent:DB_FILE];
        NSLog(@"db path : %@", dbPath);
    } else {
        //error
        NSLog(@"search Document path error. database file open error.");
        return false;
    }
    
    //DBファイルがDocument配下に存在するか判定
    NSFileManager *fileManager = [NSFileManager defaultManager];
    if (![fileManager fileExistsAtPath:dbPath]) {
        //存在しない
        //デフォルトのDBファイルをコピー(初回のみ)
        //ファイルはアプリケーションディレクトリ配下に格納されている。
        NSBundle *bundle = [NSBundle mainBundle];
        NSString *orgPath = [bundle bundlePath];
        //初期ファイルのパス。(~/XXX.app/sample.db)
        orgPath = [orgPath stringByAppendingPathComponent:DB_FILE];
        
        //デフォルトのDBファイルをDocument配下へコピー
        if (![fileManager copyItemAtPath:orgPath toPath:dbPath error:nil]) {
            //error
            NSLog(@"db file copy error. : %@ to %@.", orgPath, dbPath);
            return false;
        }
    }
    
    //open database with FMDB.
    _db = [FMDatabase databaseWithPath:dbPath];
    return [_db open];
}
更新処理
FMDBのメソッドは大きく分けて更新系のメソッドと参照系のメソッドに分かれています。

  • 更新系:- (BOOL)executeUpdate:(NSString*)sql, ...;
  • 参照系:- (FMResultSet *)executeQuery:(NSString*)sql, ...;

  •  上記メソッドは代表的なもので、この他にも引数が異なる形でいくつか提供されています。
    以下、更新系のサンプルです。トランザクションを開始している点とステートメント再利用フラグを有効にしている点に注意して下さい。
    - (BOOL)executeUpdate1 {
        BOOL result = TRUE;
        //トランザクション開始(exclusive)
        [_db beginTransaction];
        
        //ステートメントの再利用フラグ
        //おそらくループ内で同一クエリの更新処理を行う場合バインドクエリの準備を何回
        //も実行してしまうのためこのフラグを設定する。
      //このフラグが設定されているとステートメントが再利用される。
        [_db setShouldCacheStatements:YES];
        
        
        //insertクエリ実行(プリミティブ型は使えない)
    //    [_db executeUpdate:@"insert into example values (?, ?, ?, ?)",
    //                                1, 2, @"test", 4.1];
    // executeUpdateWithFormatメソッドで可能。
    
        for (int i=1; i<=30; i++) {
            [_db executeUpdate:@"insert into example values (?, ?, ?, ?)",
                                [NSNumber numberWithInt:i],
                                [NSDate date],
                                [NSString stringWithFormat:@"string : %d.", i],
                                [NSNumber numberWithFloat:(float)i/3]];
            //check
            if ([_db hadError]) {
                result = FALSE;
                NSLog(@"Err %d: %@", [_db lastErrorCode], [_db lastErrorMessage]);
            }
        }
        
        //commit
        [_db commit];
        
        return result;
    }
    
    
    トランザクションは2種類のモードが提供されており、以下メソッドが提供されています。
    - (BOOL)beginTransaction;
    - (BOOL)beginDeferredTransaction;

    ・beginTransaction
    begin exclusive transaction を指定してトランザクションを開始。
    これは読み取り、書き込み共に排他されるロックモードです。
    通常1スレッドで処理を行うのであれば、このモードで問題ありません。

    ・beginDeferredTransaction
    begin deferred transaction を指定してトランザクションを開始。
    これは必要に応じて最初のアクセス時にロックが働き、読み取りの場合は共有ロックで書き込みの場合は予約ロックがかけられるようです。因みにSQLiteのデフォルトロックモードは「Deferred」です。
    参照処理
    前述で登録したレコードを参照してみます。

    ・結果の取得をインデックスで行う場合。
    - (NSArray*)executeQuery1 {
        //結果格納用配列
        NSMutableArray *result = [[NSMutableArray alloc] init];
        
        //クエリの実行と結果(ResultSet)の取得
        FMResultSet *rs = [_db executeQuery:@"select * from example"];
        
        //結果の取得(indexで取得)
        while ([rs next]) {
            //結果格納用のオブジェクト
            ExampleRecord *record = [[ExampleRecord alloc]init];
            record.columnOfInteger = [rs intForColumnIndex:0];
            record.columnOfDate = [rs dateForColumnIndex:1];
            record.columnOfText = [rs stringForColumnIndex:2];
            record.columnOfReal = [rs doubleForColumnIndex:3];
            
            //配列に格納
            [result addObject:record];
        }
        //close ResultSet.
        [rs close];
        
        return result;
    }
    
    

    ・結果の取得をカラム名で行う場合
    - (NSArray*)executeQuery2 {
        //結果格納用配列
        NSMutableArray *result = [[NSMutableArray alloc]init];
        
        
        //クエリ実行
        FMResultSet *rs = [_db executeQuery:@"select * from example where i = ?", 
                           [NSNumber numberWithInt:2]];
    //    FMResultSet *rs = [_db executeQuery:@"select * from example where t like ?", @"%2."];
    
        if ([_db hadError]) {
            NSLog(@"Err %d: %@", [_db lastErrorCode], [_db lastErrorMessage]);
        }
        
        //結果の取得(カラム名指定)
        while ([rs next]) {
            //結果格納用オブジェクト
            ExampleRecord *record = [[ExampleRecord alloc]init];
            record.columnOfInteger = [rs intForColumn:@"i"];
            record.columnOfDate = [rs dateForColumn:@"n"];
            record.columnOfText = [rs stringForColumn:@"t"];
            record.columnOfReal = [rs doubleForColumn:@"r"];
            
            [result addObject:record];
        }
        //close ResultSet.
        [rs close];
        
        return result;
    }
    
     因みに、上のコードで登場しているExampleRecordというクラスは自前のクラスでレコードデータを格納する クラスです。レコードクラスは無理に作成する必要はありませんが、参考までに載せておきます。

    ・ExampleRecord.h
    @interface ExampleRecord : NSObject {
        int         i_;
        NSDate      *d_;
        NSString    *t_;
        double      r_;
    }
    
    // I'm using ARC.
    @property (nonatomic) int columnOfInteger;
    @property (nonatomic) NSDate* columnOfDate; 
    @property (nonatomic, copy) NSString* columnOfText;
    @property (nonatomic) double columnOfReal;
    
    @end
    

    ・ExampleRecord.m
    #import "ExampleRecord.h"
    
    @implementation ExampleRecord
    @synthesize columnOfInteger = i_;
    @synthesize columnOfDate = d_;
    @synthesize columnOfText = t_;
    @synthesize columnOfReal = r_;
    
    @end
    
    DBの解放
    - (void)closeDatabase {
        if (_db) {
            [_db close];
        }
    }
    

    ちょっと長くなってしまったので、続きは次回ということで。スレッド絡みは気になる所なので、そのあたりができたらと考えています。
    マルチスレッド上での使用を考慮したFMDatabaseQueueについては iOSでSQLiteを使う2(FMDBのFMDatabaseQueueクラスを使ってみる)の方に書いています。

    =========== 2014/4/22 追記 ===================
    FMDBを利用したサンプルプロジェクトをGitHubに上げていますので、参考にしてください。別件で作成したプロジェクトなので、若干本記事と異なる部分がありますが、初期化、参照、更新、バックグラウンド更新の処理があります。簡単なTableViewにAlertViewから入力した文字を表示するだけのアプリです。
    GitHub : https://github.com/takuran/FMDBExample
    $ git clone https://github.com/takuran/FMDBExample.git
    $ cd FMDBExample
    $ git submodule update --init --recursive
    して下さい。
    スクリーンショット:
    削除はスワイプでね。
    =============== 追記終わり ==================

    2011年11月1日火曜日

    Objective-cにおける文字列定数について

    Objective-cで文字列定数を作成する場合どうしてますか? 多分、簡単に考えるとプリプロセッサを使って以下のように
    Objective-cでの文字列定数(普通に考えると)
    #define CONST_STRING    @"const string."
    
    でも、C/C++言語的には定数のプリプロセッサ使用は好まれません、2重定義がうざいですからね。 なんというか、誰からも参照可能な反面まったく使用しないクラスにおいても同名で定義する事が 出来なくなります。
    C++での文字列定数
    因みに、C++の場合は以下のように定義しています。(Effective c++の受け売りですが)
    const char * const CONST_STRING = "const string.";
    
    要するに、実体もポインタも変更不可な変数です。
    Javaでの文字列定数
    Javaで書くとこうなるのかな。
    private static final String CONST_STRING = "const string.";
    
    Objective-cでの文字列定数(私はこうする)
    でもって、Objective-cでは文字列定数をどう定義するべきか。なのです。 因みにプリプロセッサの使用は推奨されていないみたいですね。
    [ Naming Instance Variables and Data Types ]

    C言語なので、この場合はconstを使います。が、NSStringクラス自体がImmutableなクラスですので、以下のように。
    @implementation SampleClass
    
    static NSString * const CONST_STRING = @"const string";
    
    - (id) init {
    }
    
    + (NSString*) constString {
            return CONST_STRING;
    }
    @end
    
    因みにこのstaticはこのファイル(クラス)に閉じるという意味です。ゲッタを作成してこの定数を返すようにすればJavaっぽく使えると 思います。 もちろん、文字列定数の場合です。当然かも知れませんが、数値の場合はenumの使用が推奨されているみたいですね。

    [2013/12/01 追記]
    Effective Objective-C2.0を読みました。文字列定数について言及されていましたので、 私の考えと異なっていた部分について追記します。定数の定義は前述したとおりの定義で認識合っていましたが、 他のクラス(ファイル)との共有方法が異なっていました。 Effective Objective-C2.0では以下のようにヘッダファイルに定義を追加します。上記の例だと、
    extern NSString * const CONST_STRING;
    と定義を追加します。そして、定数を使用したいクラスで本ヘッダファイルを引き込む事で直接利用可能にします。また、上記例で実装部では staticを付与していましたが、他のファイルでも参照可能にするため、static定義を削除して以下のように定義します。
    NSString *const CONST_STRING = @"const string";
    Objective-C自体がC言語仕様を継承しているので、このように記述できますが、グローバル変数として定義することになるため、 変数の命名方法に注意する必要があります。 Objective-Cでは組み込みの名前空間機能を持っていないので、同名の変数が定義されて名前の衝突が起きることが懸念されます。 名前の衝突はリンクエラーになるため、エラーになったら名前を変更すれば良いという楽観的な考えで定義してしまうと、 ライブラリのように他のモジュールから利用される事を前提としている場合は厄介なことになります。 グローバル変数を定義する場合は、特定のプレフィックスを付けて定義するすることで回避できると思いますので、 自分のプログラムとしてプレフィックスを考えて定義する必要があるでしょう。 ここでは記載しませんが、Effective Objective-C2.0では名前空間として衝突をさけるためにプレフィックス名を使用 することを推奨しており、一般的な命名則についての言及がありました。興味ある方は、以下リンクからどうぞ。
    Effective Objective-C 2.0
    Effective Objective-C 2.0
    posted with amazlet at 13.11.12
    Matt Galloway
    翔泳社
    売り上げランキング: 1,964

    2011年3月6日日曜日

    Objective-C(プロパティ)

    インスタンス変数にアクセスする方法として、GetterとSetterを自前でコードを記述し、インスタンス変数へのアクセサを提供する方法があります。これはJava/C++等のオブジェクト指向言語で一般的に利用される方法だと思います。
    ですが、インスタンス変数が多くなるにつれて、同じようなメソッドをいくつも記述する必要があり現実的ではない場合もあります。あまりにも数が多い場合にはGetter/Setterコードを出力するツールを作成したりもした経験があります。
    Objective-Cではこの面倒な作業を簡略化してくれます。全てが自動的に行われるわけではありませんが、固定的なコードの生成作業は大幅に少なくなります。

    プロパティの定義

    @property(オプション) 型 変数名;

    ※オプションは省略可能で、省略された場合デフォルト値が適用されます。

    以下、記述例。

    @property int value;            //オプションは省略
    @property(readonly) int value;  //オプション指定
    

    プロパティの定義例

    /******************/
    /* Class Prop     */
    /******************/
    @interface Prop: NSObject {
        int value1;
        int value2;
    }
    - (id)init;
    - (void)dealloc;
    - (void)printValues;
    
    //value1はGetterのみ(読み取り専用)
    //- (int)value1;
    @property(readonly) int value1;
    
    //value2はGetter/Setter両方定義
    //- (int)value2;
    //- (void)setValue2:(int)v;
    @property(readwrite) int value2;
    
    @end
    

    プロパティの実装

    プロパティの実装は@propertyを使ってインタフェースが定義されている場合に次のように記述することで、Getter/Setterメソッドを記述した事になります。

    @synthesize 変数名;

    @propertyのオプションにもよりますが、上記の定義を1行記述することで、GetterとSetter(readonlyオプションが指定されていない場合)が実装された事になります。

    プロパティの実装は以下のように複数のプロパティをカンマでつなげて定義することも出来ます。

    @synthesize value1, value2;


    プロパティの実装例

    //Propクラス実装
    @implementation Prop
    - (id) init {
        self = [super init];
        if (self != nil) {
            value1 = 1;
            value2 = 2;
        }
        return self;
    }
    - (void) dealloc {
        printf("Prop was released.\n");
        [super dealloc];
    }
    - (void) printValues {
        printf("value1 = %d, value2 = %d\n", value1, value2);
    }
    
    //プロパティvalue1の実装部
    @synthesize value1;
    //以下のアクセサを実装していることとと同等
    //Getter(value1)
    //- (int) value1 {
    // return value1;
    //}
    
    //プロパティvalue2の実装部
    @synthesize value2;
    //以下のアクセサを実装していることとと同等
    //Getter(value2)
    //- (int) value2 {
    // return value2;
    //}
    //Setter(value2)
    //- (void) setValue2: (int)v {
    // value2 = v;
    //}
    
    @end
    

    @propertyに指定可能なオプション

    種類オプション意味
    メソッド名指定getter=ゲッタ名
    setter=セッタ名
    ゲッタメソッド、セッタメソッドを明示的に指定する。
    読み書き属性readonly読み取り専用
    readwrite読み書き可能(デフォルト値)
    値の設定方法assign代入
    retain保持
    copyコピー
    アトミック性noatomicアトミック性無し
    ※オプションに関する詳細はまた別途

    プロパティへのアクセス。ドット演算子
    プロパティへのアクセスはドット演算子を利用してアクセスします。

    /****************/
    /* main         */
    /****************/
    int main(void) {
        //AutoreleasePool
        id pool = [[NSAutoreleasePool alloc] init];
        Prop *prop = [[[Prop alloc] init] autorelease];
     
        //初期値を表示
        [prop printValues];
    
        //値の取得(value1)
        //ドット演算子によるアクセス
        printf("value1 = %d\n", prop.value1);
     
        //値の取得と設定(value2)
        //ドット演算子によるアクセス
        printf("value2 = %d\n", prop.value2);
        prop.value2 = 20;
     
    
        //値を表示
        [prop printValues];
        //自動開放プールの解放
        [pool release];
        return 0;
    }
    
    
    詳解 Objective-C 2.0 第3版
    荻原 剛志
    ソフトバンククリエイティブ
    売り上げランキング: 5,145

    2011年2月18日金曜日

    そろそろXcode起動しましょうか?

    iOS SDKに触る前に、一通りObjective-Cの習得を・・・と思っていましたが、なかなか時間が取れず先に進まないっす。基本的な事は理解してきたとは思っていますが、スレッド・同期・メッセージングあたりと例外周りに関してはきちんと学びたいと考えています。とは言っても、そろそろXcode起動したくなっちゃいました。ということで、今後はObjective-Cの習得と並行してiOS SDKに触っていきたいと考えています。

    2011年2月13日日曜日

    Objective-C(プロトコル)

    プロトコルとはJavaでのインタフェースと同様に、オブジェクトの役割や振る舞いを表すメソッドの定義群の事を表します。プロトコルの定義は以下のように行います。

    @protocol プロトコル名
    メソッド宣言1;
    メソッド宣言2;
    ・・・
    @end
    

    そして、このプロトコルを採用(プロトコルを取り込む事を「採用」と言うようです)するには以下のようにします。

    @interface クラス名 : スーパークラス名 <プロトコル名> {
    インスタンス変数宣言;
    ・・・
    }
    メソッドの宣言;
    ・・・
    @end
    

    スーパクラスに対して、プロトコルを指定する事に対して違和感を覚えますが、Objective-Cは必ずスーパークラスを指定する必要があるので(前記事参照)、あまり深くは考えずにこういうもんだ程度に覚えることにします。
    以下にサンプルを載せます。サンプルはPというプロトコルをクラスAが採用しています。

    /*************/
    /* Protocol  */
    /*************/
    @protocol P
    - (void) helloWorld;
    @end
    
    /**********
     * Class A
     **********/
    @interface A : NSObject<P> {
    }
    - (id) init;
    @end
      
    @implementation A
    - (id) init {
        self = [super init];
        return self;
    }
    
    // プロトコルAの実装
    - (void) helloWorld {
        printf("Hello World.\n");
    }
    @end
    
    /**********/
    /* main   */
    /**********/
    int main(void) {
        //AutoreleasePool
        id pool = [[NSAutoreleasePool alloc] init];
     
        //普通のid型変数定義
        id classA = [[[A alloc] init] autorelease];
        //プロトコルAを採用していることを明示的に 
        id<P> classA2 = [[[A alloc] init] autorelease]; 
        //これでもOK
        A<P> *classA3 = [[[A alloc] init] autorelease]; 
    
        [classA helloWorld];
        [classA2 helloWorld];
        [classA3 helloWorld]; 
    
        //release 
        [pool release];
        return 0;
    }
    

    上記サンプルでは、プロトコルを採用したオブジェクトの型宣言として
    • id型
    • id<プロトコル名> 型
    • 具体的なクラス名型


    で定義する3パターンでの型定義で実装しています。どちらでも正しく動作します。また当然ですが、メソッドの引数に対してもプロトコルを採用したオブジェクト型を利用することが可能です。

    2011年2月10日木曜日

    Objective-C(抽象クラス)

    Javaで言うところのabstract、C++でのvirtualのような抽象クラスの言語上の仕組みはObjective-Cにはありません。ですが、言語上に制約はありませんが、継承の概念を用いて抽象クラスっぽい(?)事が出来ます。

    //Person class.
    @interface Person : NSObject {
    }
    - (id) init;
    - (void) sayName;
    - (void) whatYourName;
    @end
    
    @implementation Person
    - (id) init {
        return [super init];
    }
    
    //お名前メソッド。Personクラスでは無名ということで、名前は分かりません。
    - (void) sayName {
        printf("自分の名前分かりませーん。\n");
    }
    - (void) whatYourName {
        [self sayName];
    }
    @end
    
    //佐藤さんクラス(仮称)
    @interface Sato : Person {
    }
    - (id) init;
    - (void) sayName;
    @end;
    
    @implementation Sato
    - (id) init {
        return [super init];
    }
     //お名前メソッド 
    - (void) sayName {
        printf("私は佐藤です。\n");
    }
    @end
    
    //加藤さんクラス(仮称)
    @interface Kato : Person {
    }
    - (id) init;
    - (void) sayName;
    @end
    
    @implementation Kato
    - (id) init {
        return [super init];
    }
     //お名前メソッド
    - (void) sayName {
        printf("私は加藤です。\n");
    }
    @end
    
    //main
    int main(void) {
        id pool = [[NSAutoreleasePool alloc] init];
        //佐藤さんインスタンス、Person型で定義していることに注意。
        Person *sato = [[[Sato alloc] init] autorelease];
        //加藤さんインスタンス、Person型で定義していることに注意。
        Perpson *kato = [[[Kato alloc] init] autorelease];
    
        //普通にメソッドを呼び出し 
        [sato sayName];
        [kato sayName];
     
        //親クラスのメソッド呼び出し、サブクラスのメソッドが呼び出される
        [sato whatYourName];
        [kato whatYourName];
    
        [pool release];
        return 0;
    }
    

    実行結果

    私は佐藤です。
    私は加藤です。
    私は佐藤です。
    私は加藤です。

    メイン関数内の

    //普通にメソッドを呼び出し 
     [sato sayName];
     [kato sayName];
    

    では、親クラスの型に対して、親クラスのメソッドsyaNameが呼び出されるわけではなく、それぞれサブクラスのメソッドが正しく呼び出されています。

    //親クラスのメソッド呼び出し、サブクラスのメソッドが呼び出される
     [sato whatYourName];
     [kato whatYourName];
    

    では、親クラスにのみ定義されているwhatYourNameメソッドを呼び出し、そのメソッド内からサブクラスの呼び出しが正しく行われています。Java/C++のように言語上のサポートはありませんが、抽象クラスとしては同様の事が可能なようです。

    2011年2月9日水曜日

    Objective-C(自動開放プール2)

    自動開放プールの使い方に関してですが、自動開放プールを複数定義することで、任意の範囲でメモリの開放を制御する事が出来ます。実際の使い方としては、あるループ内で必要な処理を行い、ループ毎にメモリの取得・開放を行いたい場合は以下のようなサンプルになるかと思います。メイン関数ではプログラムの開始と終了で自動開放プールを宣言し、whileループ内ではループの頭に自動開放プールを宣言し、ループを抜ける最にリリースを行っています。このように複数の自動開放プールを定義することで、必要な範囲で自動開放プールを利用することが可能です。

    /***********/
    /* Class A */
    /***********/
    @interface A : NSObject {
    }
    - (void) dealloc;
    
    @end
    
    @implementation A
    - (void) dealloc {
        printf("I'm released.\n");
        [super dealloc];
    }
    @end
    
    /**********/
    /* main   */
    /**********/
    int main(void) {
        //自動開放プールの生成
        id releasePool = [[NSAutoreleasePool alloc] init]; 
        A *aInstance = [[A alloc] init];
        //自動開放プールに登録
        [aInstance autorelease];
     
        int index=5;
        while (index-- > 0) {
            //ループ毎の自動開放プールを宣言
            id rpw = [[NSAutoreleasePool alloc] init];
            id a = [[A alloc] init];
            //自動開放プールに登録。このメモリはwhileループ毎に開放される
            [a autorelease];
     
            //ループ単位で自動開放プールの開放を行う。
             [rpw release];
        }
    
        //自動開放プールの開放。ここで登録した全てのインスタンスにreleaseメッセージが送信される。 
        [releasePool release];
        return 0;
    }
    

    以下、実行結果。ループに入る前に生成した1つとループ内で生成した5つのインスタンスが正しく解放されています。

    I'm released.
    I'm released.
    I'm released.
    I'm released.
    I'm released.
    I'm released.

    2011年2月6日日曜日

    Objective-C(自動開放プール)

    iOS4で動作するObjective-Cではガベージコレクションがサポートされておらず、代わりにカウンタ管理方式を利用する必要があるのですが、生成したインスタンスすべてに対してreleaseメソッドを呼び出すのは面倒ですし、忘れてしまう可能性もあり使い勝手があまり良くないんじゃないかと。で、そんな時に利用するのが「自動開放プール」となります。

    自動開放プールプールの簡単な使い方は以下です。

    #import <Foundation/NSObject.h>
    #import <Foundation/NSAutoreleasePool.h> //自動開放プール
    #import <stdio.h>
    
    /***********/
    /* Class A */
    /***********/
    @interface A : NSObject {
    }
    - (void) dealloc;
    
    @end
    
    @implementation A
    - (void) dealloc {
      printf("I'm released.\n");
      [super dealloc];
    }
    
    @end
    
    /**********/
    /* main   */
    /**********/
    int main(void) {
      //自動開放プールの生成
      id releasePool = [[NSAutoreleasePool alloc] init]; 
      A *aInstance = [[A alloc] init];
      //自動開放プールに登録
      [aInstance autorelease];
    
      //自動開放プールの開放。ここで登録した全てのインスタンスにreleaseメッセージが送信される。 
      [releasePool release];
      return 0;
    }
    

    以下、実行結果。
    I'm released.

    自動開放プールはNSAutoreleasePoolとして定義されています。使い方としては、NSAutoreleasePoolはインスタンス生成し、メモリの開放を行いたい所(たいていは何処かのスコープを抜ける所だと思いますが)で自動開放プールをreleaseすることで開放処理が行われます。
    自動開放プールにインスタンスを登録するには、生成したインスタンスに対して「autorelease」メソッドを呼び出すことで登録を行います。autoreleaseメソッドはNSObjectに定義されています。

    2011年2月5日土曜日

    Objective-C(メモリ管理方式)

    カウンタ管理方式とガベージコレクション

    Objective-Cはメモリ管理方式として、
    • カウンタ管理方式
    • ガベージコレクション
    が用意されています。
    カウンタ管理方式はオブジェクトの参照カウンタにより、最終的にオブジェクトへの参照が無くなってからメモリを開放する方式です。但し、カウンタの操作はプログラマが行う必要があります。
    ガベージコレクションは簡単に言うと不要になったオブジェクトを探し出し、自動的に開放してくれるシステムです。Java等はガベージコレクションが採用されていると思います。ガベージコレクションと言っても、様々な実装方式があり、細かいことは省略したいと思います。

    iOS4のメモリ管理方式

    なんと、個人的には驚いたのですが、iOS4のメモリ管理方式としては「カウンタ管理方式」しか提供されていないとのこと。iOS4でガベージコレクションは利用出来ないのです。

    リファレンスカウンタの操作

    リファレンスカウンタの操作には以下の3つのメソッドが提供されており、NSObjectクラスにて定義されています。

    メソッド名説明
    - (id) retain リファレンスカウンタをインクリメント
    - (oneway void) release リファレンスカウンタをデクリメント
    - (void) dealloc メモリ開放時に呼ばれるメソッド

    以下、サンプルコード。

    #import 
    #import 
    
    @class B;
    
    //Class A
    @interface A : NSObject{
      B* bInstance;
    }
    - (id) init;
    - (void) dealloc;
    - (void) bInstance: (B*) inst;
    
    @end
    
    @implementation A
    //初期化メソッド
    - (id) init {
      self = [super init];
      bInstance = nil;
      return self;
    }
    
    //メモリ解放時に呼び出されるメソッド
    - (void) dealloc {
      printf("class A : dealloc called.\n"); 
      if (bInstance != nil) {
        //クラスA自体が開放されるため、保持しているインスタンス変数の
        //リファレンスカウンタデクリメントを行う
        [bInstance release]; //リファレンスカウンタをデクリメント
      }
      //必ず呼び出す必要がある
      [super dealloc];
    }
    
    //クラスAのインスタンス変数にクラスBのインスタンスを設定
    - (void) bInstance: (B*) inst {
      bInstance = inst;
    }
    @end
    
    //Class B
    @interface B : NSObject {
    }
    - (id) init;
    - (void) dealloc;
    
    @end;
    
    @implementation B
    - (id) init {
      self = [super init];
      return self;
    }
    
    - (void) dealloc {
      printf("class B : dealloc called.\n");
      [super dealloc];
    }
    @end;
    
    //main
    int main(void) {
      A *aInstance = [[A alloc] init]; //インスタンスA生成
      B *bInstance = [[B alloc] init]; //インスタンスB生成
    
      //クラスAのインスタンス変数にクラスBのインスタンスを設定
      //クラスBのリファレンスカウンタをインクリメントしてから受け渡す
      [aInstance bInstance: [bInstance retain]]; 
    
      [aInstance release]; //リファレンスカウンタデクリメント
      [bInstance release]; //リファレンスカウンタデクリメント
      return 0;
    }
    

    実行結果
    class A : dealloc called.
    class B : dealloc called.

    2011年2月2日水曜日

    Objective-C(メソッドのオーバーロード)

    Objective-CもC++やJavaと同様にオーバーロードが存在しますがはオーバーロードっぽいことが出来ますが、言語仕様としてのオーバーロードは存在しません。本記事を見るとラベル名を変更すればオーバーロード出来ると記載してますが、Objective-Cでラベル名を変更するということは、異なるメソッドを定義している事にすぎません。
    Reference : Method overloading in Objective-C?(stackoverflow)
    (誤解を招く記載のため修正 2014/02/08 ナヨユキ)

    引数の数が異なるメソッドのオーバーロード

    単純な例を示します。クラスAが存在し、それぞれ引数の数が異なるメソッドを3つ用意し、main関数内でぞれぞれの引数が異なるメソッドを呼び出してみます。

    /*************/
    /* class A ***/
    /*************/
    @interface A : NSObject {
    }
    - (int) test1;
    - (int) test1: (int) a1;
    - (int) test1: (int) a1 arg2: (int) a2;
    
    @end;
    
    @implementation A 
        //オーバーロードメソッド1(引数無し)
        - (int) test1 {
            printf("wow1.\n");
        }
    
        //オーバーロードメソッド2(引数1つ)
        - (int) test1: (int) a1 {
            printf("wow2. arg1 = %d.\n", a1); 
        }
    
        //オーバーロードメソッド3(引数3つ)
        - (int) test1: (int) a1 arg2: (int) a2 {
            printf("wow3. arg1 = %d. arg2 = %d\n", a1, a2);
        }
    @end
    
    /**********/
    /* main ***/
    /**********/
    int main(void) {
        A* classA = [[A alloc] init];
        [classA test1];              //引数無し
        [classA test1: 1];           //引数1つ
        [classA test1: 1 arg2: 2];   //引数2つ
    
        return 0;
    }
    

    実行結果

    wow1.
    wow2. arg1 = 1.
    wow3. arg1 = 1. arg2 = 2

    それぞれ引数の異なるメソッドを正しく呼び出せています。引数の数が異なる場合は問題なくオーバーロードが出来るようです。このあたり、Java/C++とは同じだと思います。

    引数の型が異なるメソッドのオーバーロード

    次に引数の型が異なるメソッドを定義してみます。

    /*************/
    /* class A ***/
    /*************/
    @interface A : NSObject {
    }
    - (int) test1;
    - (int) test1: (int) a1;
    
    //オーバーロード(int型の引数)
    - (int) test1: (int)    a1 arg2: (int)    a2; 
    
    //オーバーロード(double型の引数) 
    - (int) test1: (double) a1 arg2: (double) a2; 
    @end;
    

    このようにすると、コンパイル時に以下のエラーが出力されました。

    error: duplicate declaration of method ‘-test1:arg2:’

    むむむ。どうやら型で区別はされていないみたい。C++/Javaの場合はこのように型が異なる場合のオーバーロードは可能ですが、Objective-Cではこれが出来ないようです。では、型ではなく引数のラベル名を変更してみます。以下サンプルのオーバーロードメソッド3、4、5が該当します。
    メソッド3と4は引数の型は同じで、ラベルが異なるパターン。
    メソッド4と5は引数の型も、ラベルも異なるパターンになります。

    /*************/
    /* class A ***/
    /*************/
    @interface A : NSObject {
    }
    - (int) test1;
    - (int) test1: (int) a1;
    
    //オーバーロード3(ラベルが「arg2」)
    - (int) test1: (int) a1  arg2: (int) a2;   
    
    //オーバーロード4(ラベルが「argg2」)
    - (int) test1: (int) a1 argg2: (int) a2;
    
    //オーバーロード5(ラベルが「arggg2」)
    - (int) test1: (double) a1 arggg2: (double) a2;
       
    @end;
    
    @implementation A 
        // オーバーロードメソッド1
        - (int) test1 {
            printf("wow1.\n");
        }
    
        // オーバーロードメソッド2
        - (int) test1: (int) a1 {
            printf("wow2. arg1 = %d.\n", a1); 
        }
    
        // オーバーロードメソッド3
        - (int) test1: (int) a1 arg2: (int) a2 {
            printf("wow3. arg1 = %d. arg2 = %d\n", a1, a2);
        }
    
        // オーバーロードメソッド4
        - (int) test1: (int) a1 argg2: (int) a2 {
            printf("wow4. arg1 = %d. arg2 = %d\n", a1, a2);
        }
    
        //オーバーロードメソッド5
        - (int) test1: (double) a1 arggg2: (double) a2 {
            printf("wow5. arg1 = %f. arg2 = %f\n", a1, a2);
        }
    
    
    @end
    
    /**********
     * main
    **********/
    int main(void) {
        A* classA = [[A alloc] init];
        [classA test1];
        [classA test1: 1];
        [classA test1: 1   arg2: 2]; //オーバーロード
        [classA test1: 3  argg2: 4]; //オーバーロード
        [classA test1: 5 arggg2: 6]; //オーバーロード
    
        return 0;
    }
    
    この場合、コンパイル時にはエラーが出ませんでした、
    実行結果

    wow1.
    wow2. arg1 = 1.
    wow3. arg1 = 1. arg2 = 2
    wow4. arg1 = 3. arg2 = 4
    wow5. arg1 = 5.000000. arg2 = 6.000000

    問題無く実行出来ています。結局の所、Objective-Cは型ではなく、メッセージセレクタ(ラベル)の違いによりオーバーロードが可能ということみたいです。Java/C++では引数の数、型で区別しているので、逆に同一の型の場合はオーバーロード出来ません。このあたり、使い方によっては便利(?)なのかもしれませんね。

    セレクタのパラメータを省略したらどうなるんでしょ?

    /**********
     * main
    **********/
    int main(void) {
        A* classA = [[A alloc] init];
        [classA test1];
        [classA test1: 1];
        [classA test1: 1 arg2: 2];  //オーバーロード
        [classA test1: 3 argg2: 4]; //オーバーロード
        [classA test1: 5 : 6];      //オーバーロード ←ココ
    
        return 0;
    }
    

    コンパイル時に以下のワーニングが出力され、

    overload.m: In function ‘main’:
    overload.m:49: warning: ‘A’ may not respond to ‘-test1::’
    overload.m:49: warning: (Messages without a matching method signature
    overload.m:49: warning: will be assumed to return ‘id’ and accept
    overload.m:49: warning: ‘...’ as arguments.)

    そのまま、無視して実行してみると、実行時例外で落ちました。

    なんとなく分かりました。省略した場合はid型になるらしいので、そんなメソッドは無いって事ですね。きっと。そして、実行しても存在しないから落ちるってことだと思います。

    2011年2月1日火曜日

    Objective-C(メソッド定義)

    クラスメソッドとインスタンスメソッド

    クラス・メソッドの定義はヘッダファイルに@interface から始まり@endで終了するブロック内に定義します。メソッドの先頭に付いている「 - 」ですが、これにはちゃんと意味があって、「 - 」がインスタンスメソッド、「 + 」がクラスメソッドという区別になります。

    修飾子意味
    - インスタンスメソッド
    例)[instance method];
    + クラスメソッド。javaのstaticなメソッドと同等です。NSObjectクラスのallocメソッド等がこれに該当します。
    例)[Class method];

    メソッドのオーバーライド

    Objective-CもC++やJavaと同じようにメソッドのオーバーライドがが可能です。特に説明するまでも無く、サンプルを見ていただければ理解出来ると思います。

    #import <Foundation/NSObject.h>
    #import <stdio.h>
    
    @interface A : NSObject {
        int a;
        int b;
    }
    - (id) init;
    - (int) add;
    
    @end
    
    @implementation A 
    - (id) init {
        self = [super init];
        if (self != nil) {
            a = 1;
            b = 5;
        }
        return self;
    }
    - (int) add {
        return a + b;
    }
    @end
    
    
    @interface B : A {
    }
    - (id) init;
    - (int) add;    //オーバーライド
    
    @end
    
    @implementation B
    - (id) init {
        self = [super init];
    }
    
    //オーバーライド
    - (int) add {
        return a + b + 10;
    }
    @end
    
    int main(void) {
    
        A *classA = [[A alloc] init];
        int value = [classA add];
    
        B *classB = [[B alloc] init];
        int value2 = [classB add];
    
        printf("Class A value : %d\n", value);
        printf("Class B value : %d\n", value2);
    
        return 0;
    }
    
    オーバーライドサンプルコード

    以下、実行結果
    Class A value : 6
    Class B value : 16


    上のクラスはクラスBはクラスAを継承しており、クラスBのメソッド「add」はクラスAのメソッド「add」をオーバーライドしています。
    オーバーライドメソッドからスーパークラスのメソッドを呼び出すにはこのようにします。
    - (int) add {
        return [super add]; //スーパークラスのメソッドを呼び出し
    }
    
    スーパークラスメソッドの呼び出し

    このあたり、はc++/Javaと同様ですので、すんなり理解できます。

    2011年1月30日日曜日

    Objective-C(インスタンス生成)

    インスタンス生成と初期化
    Objective-Cでのインスタンス生成について、
    Carというクラスのインスタンス生成は以下のように行います。

    id carInstance = [[Car alloc] init];

    idは特別な型で、すべてのクラスはこの型で表すことが出来ます。なんでもアリです。void* 型的なイメージだと思います。もちろんC++的にクラスの型を使って表すことも出来ます。

    Car *carInstance = [[Car alloc] init];

    インスタンスの生成は alloc です。initは初期化用のメソッドとなります。このあたりの詳細は利用するクラスリファレンスを参照すれば良いと思いますが、Cocoa環境ではinitが初期化という事になっているらしいです。現状はインスタンス生成はallocしてinit程度に覚えておけば良いと思います。

    2011年1月28日金曜日

    Objective-C(クラスと継承)

    Objective-Cにおけるクラス定義

    クラス定義は以下のように行います。主にヘッダファイル(*.h)に記述する内容です。
    @interface クラス名 : スーパークラス名 { インスタンス変数定義; } メソッド宣言; @end

    まあ、なんとなくイメージは付きますが、大括弧のスコープの外でメソッドの宣言をする構文は少し違和感があります。
    以下、例です。
    @interface Car : NSObject {
            int distance;
    }
    - (id) init;
    @end
    
    図1.クラス定義サンプルコード

    @interfaceで始まりインスタンス変数は見たままで、メソッドの定義が大括弧の後に続きます。そして、最後に@endで終わりです。
    NSObjectはスーパークラスでJavaで言うところのObjectです。Javaは無条件で継承されますがObjective-Cは明示的に記述する必要があるようです。また、Objective-Cではすべてのクラスはルートクラスを継承する必要があり、Cocoa環境ではルートクラスとしてNSObjectとNSProxyというクラスが定義されているようです。とりあえずは、NSObjectをルートクラスとしておけば良いと思います。

    Objective-Cにおけるメソッド定義

    上記図1サンプルにある、

    - (id) init;

    はid型を返却する init というメソッド定義になります。id型はすべてのオブジェクトで利用可能なオブジェクト型で、C言語でのvoid*型に似ていると思います。メソッドに引数がある場合は以下のようになります。

    - (id) sampleMethod: (int) a1;

    では引数が2つ以上の場合はどうなるかというと

    - (id) sampleMethod: (int) a1 arg2: (int) a2;

    となります。ちょっと?ですよね。第1引数はいいのですが、第2引数は「arg2」とかいうのが付いてます。これはキーワード(ラベル)です。省略も可能なようですが、コンパイル時に警告がでます。
    今度はメッセージを送信する(メソッド呼び出し)場合はどうするのでしょう、以下のようにになります。

    [car sampleMethod: 1 arg2: 2];

    第2引数はキーワードを指定してから引数を:の後に記述します。なんかちょっとややこしいですが、メッセージのキーワードはソースを読む側からするとその引数が何を意味しているのかが、一発で理解できると思うので、慣れたらこれはこれでメリットがあると思います。

    コマンドラインでのコンパイル方法

    Xcodeでコンパイルすれば良いのかもしれませんが、コマンドラインからのコンパイルも出来ます。私はiOS SDKのクラスを使うまでは(どこまでがiOS SDKのクラスかはわかってないですけどw)コマンドラインからコンパイルしています。エディタはvimでターミナル上で編集しています。

    [bash ]# cc ソースファイル.m -framework Foundation

    としてコンパイルを実施しています。オプションは基本gccのオプションと同じようなイメージ。ですので、デバッグオプションは -g です。試してみましたが、gdbで普通にデバッグできました。デバッガに関しては特に書きませんが、無理してターミナル上でgdbを使う必要は無いとおもいます。素直にXcodeを使えばよいかと。

    2011年1月27日木曜日

    Objective-C(メッセージ)

    Objective-Cの知識が無い状態で、いきなりiOS SDKを学ぶことは悪いことはあっても良いことは無いと思うので、ある程度のObjective-Cの基本的な知識をつけてからiOS SDKに入っていきたいと思っています。
    そして、Objective-Cの基本として、まずはメッセージです。C/C++/Javaを知っている人はこのメッセージ式が今までと記述的に全く異なります。

    メッセージ

    Objective-Cではオブジェクトに対してメッセージ(メソッド呼び出し)を以下のように表現します。

    [object message]

    例えば、Carいうクラスがあってそのオブジェクトがcar、そのクラスにmoveというメッセージ(メソッド)を送る場合は、

    [car move];


    という感じ。Java/C++使いの方はいきなり違う(汗)って感じですよね。まあ、記述方式が違うだけですが。
    そして、引数の渡し方も異なります。引数はメッセージの後にコロンを付けて指定します。

    [car move:10];

    carオブジェクトにmoveというメッセージを10という引数をつけて送信。
    引数が1つの場合はまだイメージ出来るのですが、2つ目から少し違います。

    [car move:10 arg2:20 arg3:30];

    のような感じです。第一引数は値だけですが、第二引数からはキーワード(ラベル)を付与します。キーワード(ラベル)とはここではarg2, arg3の事を指します。文法上はキーワードの省略が可能ようですが、一般的には付けてるみたいです。
    まあ、慣れだとは思いますが、記述方式が思っていたよりC++/Javaとは異なっているのには驚きました。そして、今後に登場する「オーバーロード」の機能に関しては、このラベルが重要な役割を果たしていることに気がつくことになります。今のところは、こんな感じで記述するだ程度で問題ないと思います。