こんにちは、ponpoko1968です。 最近、「自炊」と言う言葉がはやっているのをご存じでしょうか?高性能で使いやすいドキュメントスキャナーが登場したことで、紙の本を断裁して、スキャンすることで電子化しPCやiPad,kindleのようなタブレットで読むことが流行しています。 私も、日々会社と家を往復する日々をすごすため、たくさんの重い技術書を持ち歩けず、何か疑問があって、調べたい、勉強したいというときに、すぐ手元に技術書があって、どこでも参照できたらなぁ。。。と常々思っていました。 そんな中、「自炊」とiPadの登場です。筆者も早速重い技術書をスキャンして、iPadで持ち歩くようになりました。おかげで過去の雑誌の記事や、解説書など、紙媒体の状態では重くて両手で持つことすら出来ない量の資料を1kgにも満たないiPadの中に入れて持ち運び出来るようになりました。 これら資料は、iPad向けに数多くリリースされている電子書籍リーダアプリを使って読んでいます。それぞれすばらしい機能があるのですが、なかなか筆者のニーズにはぴったり来るものがないなぁ、、、と思っていると、むくむくと「車輪の再発明」欲がわいてきてしまいました。そこで、せっかくだから(笑)電子書籍アプリの開発に挑戦してみることにしました。 今回の記事ではアプリのプロジェクトの構成の説明にはじまり、文書ファイルの一覧までを実装します。

2 コンセプト

私が考えた電子書籍リーダーのコンセプトを一言で言うと、「勉強やアウトプットが目的で読書する人のためのリーダー」ということになります。 というわけで自分で自分にRFPを出すと、こんな感じです。
  • 書籍データは(主に自炊した)PDFファイルをターゲット
    • PDFはリフロー型(*)の書籍形式ではないので、部分拡大機能を充実させる
  • 重要な部分などを簡単にクリップすることが出来、クリップした内容はローカルやクラウド上でコレクションできる
  • 地下鉄など電波が届かない場所で読むことも多いと思うので、機能実装においてはオフライン状態でつかうことも意識する
  • 描画・データ管理を工夫して高速・快適なブラウジングが出来るようにする
  • せっかく作るので、風呂敷を広げすぎない程度に、自分の想像力の範囲で、抽象化を意識したクラス設計にはする
当面の目標として、下記のような画面構成を考えます。

3 方針と設計

早速ですが設計に入ります。 今回のアプリはスタンドアロンのGUIアプリなのでシンプルなMVCに分解して考えます。 モデル系のクラスとしては、
  • BookShelfクラス 書籍データ全体を保持するSingletonなクラス。ディスクもしくはローカルRDBMSへのアクセスを抽象化する。永続化にはCoreDataを使ってみる予定
  • Documentクラス 個々の書籍データを保持するクラス。アプリで主に使う書籍データの実体はPDFファイルだが、目次やクリップ情報など、書籍単位で管理するデータの保持
  • Pageクラス ページの情報、ページイメージのキャッシュなど付加的なページのデータを保持。 という風に、 情報の粒度を基準にまとめることにします。

4 プロジェクトの作成

Xcodeでプロジェクトを作成します。[ファイル]→[新規プロジェクト]→[新規プロジェクトのテンプレートを選択:]で、「Navigation-Based Application」を選びます。実装に入る前に、忘れないように若干の設定変更を行います。

4.1 ファイル転送の設定

文書ファイルのアプリへの転送には、いろいろな方法が考えられますが、まずは今回はもっとも手っ取り早い、iTunesから転送する方法をサポートすることにします。 iTunesから文書ファイルをアプリへ転送できるようにするには、プロジェクトファイルの中の<アプリ名>-Info.plistに「Application supports iTunes file sharing(UIFileSharingEnabled)」と言うキーを追加して、その値を「YES」に設定します。 すると、iTunesにiPadを接続して、iPadを選択すると、[App]タブの画面下部にファイル転送用の画面が出現します。転送先のアプリのアイコンを選択して、PDFファイルをドラッグ&ドロップすれば即座にファイルが転送されるようになります。

5 Bookshelfクラスの実装

Bookshelfは転送された文書全体を管理するクラスです。シングルトンとして設計してアプリ全体からアクセスできるようにします。シングルトンなBookshelfのインスタンスはstatic変数としてクラスの実装内部で宣言します。
static Bookshelf* theBookshelf = nil;
常に単一のインスタンスを返す、sharedBookshelfというクラスメソッドを定義して、各Viewクラスはこのメソッドを起点に文書データにアクセスします。
  + (Bookshelf*) sharedBookshelf {
  if( nil == theBookshelf ){
    Log(@"%s:%d ", __FUNCTION__, __LINE__ );

    theBookshelf = [[Bookshelf alloc] init];
    UIApplication *app = [UIApplication sharedApplication];
    [[NSNotificationCenter defaultCenter] addObserver:
                                            [Bookshelf class]
                                          selector:
                                            @selector(sharedBookshelfWillClose:)
                                          name:
                                            UIApplicationWillTerminateNotification
                                          object:
                                            app];
  }
  return theBookshelf;
}
シングルトンの実装で課題となりがちなのが、シングルトンインスタンスの解放のタイミングですが、今回は、ノーティフィケーション機構を使って解放するようにしました。上記コードのように、アプリケーションが終了するタイミングで、[Bookshelf class]で取得されるクラスオブジェクトに、sharedBookshelfWillClose:メッセージを送ってもらうよう、ノーティフィケーションセンターに登録しておきます。呼び出されるsharedBookshelfWillCloseの中身は、現状はシンプルにインスタンスを解放するだけです。
+ (void) sharedBookshelfWillClose:(NSNotification*)notification {
 notification = notification;
 // シングルトンインスタンスの開放
 // UIApplicationのUIApplicationWillTerminateNotificationノーティフィケーションで実行
 Log(@"%s:%d ", __FUNCTION__, __LINE__ );
 if( theBookshelf)
   [theBookshelf release];
 }

6 文書の列挙

Bookshelfクラスはその名の通り文書を保持するクラスなので、下記のメソッドを定義します。
  • (NSInteger )documentsCount 文書数を返します。
  • (NSArray*) documentNames 文書の名前を格納したNSArrayを返します。
  • (Document*)documentWithName 文書の名前で指定されたDocumentsクラスのインスタンスを返す。 今回のアプリではアプリのバンドルのDocumentsディレクトリの直下にフラットに配置することにして、ファイル名を主キーにして、文書を識別することにします。アプリに転送された文書ファイルを列挙する処理については、サンプルコードのloadDocumentsメソッドを参照してください。

7 Documentsクラスの実装

DocumentクラスはおのおののPDFファイルへのアクセスを抽象化します。 実装と行っても、現段階のコードではファイル名を保持するだけのものなので、至ってシンプルに、2つのメソッドしか持ちません。
@interface Document : NSObject {
  NSString* filePath_;
}
-(id) initWithPath:(NSString*)path;
@property ( nonatomic, readonly, getter=getName ) NSString* name;

8 文書リストの表示

BookshelfViewControllerにコードを追加します。プロジェクト作成時点で、XCodeが生成する最初のビュー画面はすでにUITableViewになっているので、このビューをそのまま使います。
このビュー、プロジェクトの作成時のデフォルトはRootViewControllerという名前になっていますが、サンプルコードではBookshelfViewControllerという名前に変更しています。クラス名の途中変更はXCodeのリファクタリング機能を使うと便利です。
UITableViewで内容を表示するために必要な3つのメソッドの実装を書きます。 numberOfSectionsInTableViewはテーブルのセクション数を指定します。今回はテーブルをセクション分けて表示しないので、常に1を返します。
// Customize the number of sections in the table view.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}
tableView:numberOfRowsInSection: でbookShelfが持つ現在の文書数を返します。
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
   Bookshelf* shelf=   [Bookshelf sharedBookshelf];
   Log(@"%s:%d %d", __FUNCTION__, __LINE__,[shelf documentsCount] );

   return [shelf documentsCount];
}
tableView:cellForRowAtIndexPath:でファイル名を表示するセルを作って返します。
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    static NSString *CellIdentifier = @"Cell";
   Bookshelf* shelf=   [Bookshelf sharedBookshelf];

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
    }
    cell.textLabel.text = [[shelf documentNames] objectAtIndex:indexPath.row];

    return cell;
}
ちなみに、iPhoneシミュレータ上のアプリにPDFファイルを転送するには、下記のコードで取得されるパスに、直接ファイルをコピーすればOKです。 NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *aDirectory = [paths objectAtIndex:0];
これでようやく、文書のリストが画面表示されるようになりました。 次回はPDFページの表示を行います。 サンプルコードはこちら