iOS で SQLite – FMDB の使い方 2017

2017年1月22日 0 開発 , , ,

2011 年に書いた iOS で SQLite – FMDB の使い方という記事へ現在も結構なアクセスがある。

しかし当時は ARC すらなくサンプルとして古すぎる。なにしろ 5 年も前だし。また最近 iOS アプリ開発に戻ってきたこともあり、2017 年度の開発環境を学ぶ題材としてサンプルを再実装してみた。プロジェクトは GitHub に公開してある。

サンプル再実装における考察などは以下にまとめる。

開発方針

再実装にあたり開発方針をまとめる。

  • サンプル プロジェクトの機能と UI は元記事の内容を踏襲
  • Objective-C と Swift の両方を実装
  • FMDB は CocoaPods で管理
  • ユニット テストを実装する

なるべく最新の Objective-C と Storyboard を使用するのは当然として、Swift 版も実装する。Swift は 2014 年に発表されてから破壊的な変更を繰り返してきたが、Swift 3 で一段落ついたと認識している。

Swift 3の開発の振り返りとSwift 4の計画が記されたメールの紹介 – Qiita と冒頭で紹介されているメーリング リストを読むに、Swift 4 では互換性が重視され Swift 1 〜 3 のような構文レベルの大変更は抑止されるだろう。というわけで今こそ Swift 入門のチャンスと判断した。

サンプルは Objective-C、Swift 共に同等の内容とする。馴染みある Objective-C から先に実装してそれを Swift へ移植。なるべく Swift 的に好ましい機能や記法を採用するがオブジェクトやメソッド定義の変更をともなうレベルの差分は控える。

FMDB のインストールは CocoaPods を採用。元記事では FMDB のソースをプロジェクトにコピーしていたが、この方法だとバージョン管理に難がある。パッケージ管理が利用可能ならそちらへ任せるほうがよい。

あわせて Xcode の提供するユニット テスト機能も試す。

FMDB とは?

iOS アプリ開発において、SQLite を扱いやすくするためのライブラリ。Apple 的には Core Data 推しであり Xcode の GUI からテーブル編集可能などの優遇措置がある。

だが Core Data は SQLite を基本的に隠蔽している。そのため他のプラットフォームで培った SQL 知見を活かすには素で操作したくなる。特に iOS/Android 両対応のアプリを開発する場合、DB 設計と SQL 文を共用したい場面もあるだろう。

しかし SQLite は C 言語で実装されているため、Objective-C や Swift から利用しようとすると API や接続状態の管理が実に厄介だ。FMDB はこの辺をわかりやすく面倒みてくれる。

要は JDBC とか Android でいう SQLiteDatabase、.NET の System.Data.SQLite みたいなものだ。クライアント言語によりそった簡易なデータベース接続と操作 API を提供してくれる。

CocoaPods と FMDB

CocoaPods による FMDB インストールの詳細は CocoaPods を試すにまとめたので、そちらを参照のこと。サンプル プロジェクトのリポジトリには Podfile を定義してあるので、これを clone してから

$ pod install

すれば FMDB をインストールできる。その後に *.xcworkspace ファイルを Xcode で開けば FMDB への参照がプロジェクトに設定された状態となっている。

Xcode で iOS アプリを開発する場合、現在は以下のパッケージ管理を利用できる。これらから CocoaPods を選んだ理由について。

CocoaPods は Objective-C と Swift に両対応している。

Carthage は CocoaPods よりも先に Swift 対応したことで注目を集めたシステムで、機能も簡素である。CocoaPods の不満点を解消するために生まれたらしい。

Swift Package Manager は Swift 用である。Swift は OSS 化されているため iOS アプリ以外の開発でも採用される可能性がある。そのため iOS や macOS とは独立したパッケージ管理となっているようだ。

これらのうち今回は Objective-C から利用可能で知見も多い CocoaPods を選んだ。CocoaPods は 2016/5 に v1.0 がリリースされ、Podfile まわりで互換問題もあったようだが現在は落ち着いている。つまり当面は安定するだろう。これも採用理由である。

CocoaPods でインストール可能な FMDB には複数の仕向けがある。ccgus/fmdbe の CocoaPods 欄から引用。

pod 'FMDB'
# pod 'FMDB/FTS'   # FMDB with FTS
# pod 'FMDB/standalone'   # FMDB with latest SQLite amalgamation source
# pod 'FMDB/standalone/FTS'   # FMDB with latest SQLite amalgamation source and FTS
# pod 'FMDB/SQLCipher'   # FMDB with SQLCipher

今回は FMDB/FTS を採用する。FTS というのは全文検索モジュールのこと。SQLite FTS3 and FTS4 Extensions に詳しい。特別なテーブル内の TEXTMATCH で高速に検索可能となる。

この機能はオプションなので不要ならば使わなくてもよい。おなじみの SQLite へ便利機能が追加された程度であり、普通に使う分には FTS を意識することはないだろう。

DAO と RAII

FMDB はデータベース接続と SQL 文を実行する API を提供するのだが、その知識をカプセル化するため DAO ( Data Access Object ) パターンを採用。FMDB API を利用する DAO クラスと、DAO インスタンスの生成を担当する Factory クラスを用意する。

FMDB におけるデータベースは FMDatabase - databaseWithPath などのメソッドから得られた FMDatabase インスタンスとなる。データベース接続する場合はこのインスタンスに対して open、接続を閉じるなら close メソッドを呼ぶ。

NSString   *path = @"データベース ファイルのパス";
FMDatabase *db = [FMDatabase databaseWithPath:path];
if ([db open]) {

  // ...データベース操作

  [db close];
}

データベース接続を安全に管理するため open/close を対応させる必要がある。簡単なのは対応をメソッドでカプセル化することだ。なにかデータベースを操作したくなったらメソッドを追加し、その中で open/close を完結させる。

しかしこの方法だと複数の操作を連続実行したい場合、その単位でカプセル化するか毎回 open/close することを覚悟してメソッドを複数実行することになる。元記事のサンプルではそうしていた。

今回はこれを避けるため、DAO クラスのインスタンス生成と破棄を open/close に対応させる。Objective-C は init/dealloc、Swift なら init/deinit で open/close を処理。いわゆる RAII ( Resource Acquisition Is Initialization ) 的な管理である。

Objective-C と Swift はインスタンス生成と破棄を明確にハンドリングできるため RAII と相性もよい。ARC ( Automatic Reference Counting ) を利用していても参照カウンター管理を自動化するだけである。インスタンス生成したスコープで参照が完結するなら、そこを抜けたときにカウンターがゼロとなり「インスタンス破棄 = データベース接続を閉じる」ことになる。

例えば以下のように DAO クラスを定義して

- (void)init:(FMDatabase *)db {
    if (!(db)) { return nil; }

    self = [super init];
    if (self) {
        self.db = db;
    }

    return self;
}

- (void)dealloc {
    [self.db close];
}

- (Book *)add:(NSString *)author title:(NSString *)title releaseDate:(NSDate *)releaseDate {
}

- (NSArray *)read {
}

DAO Factory クラスでは

- (BookDAO *)bookDAO {
    return [[BookDAO allo] init:[self connection]];
}

- (FMDatabase *)connection {
    FMDatabase* db = [FMDatabase databaseWithPath:self.dbFilePath];
    return ([db open] ? db : nil);
}

というようにデータベース接続を済ませた FMDatabase を渡してインスタンス生成する。このメソッドから返された DAO インスタンスはスコープを抜けるまで add や read を繰り返しても同じデータベース接続が使いまわされる。

- (void)sample {
    // データベースが open される
    BookDAO *dao = [self.daoFactory bookDAO];

    // DAO を使用したデータベース操作

    // DAO インスタンス破棄 & データベース接続を閉じる
}

イメージとしては上記のような感じ。

Objective-C から FMDB を利用する

FMDB 関連の API を参照する場合は

#import <FMDatabase.h>
#import <FMResultSet.h>

のように import する。CocoaPods でインストールした場合はフレームワーク扱いとなる。面倒なのでフレームワーク名を省略しているが、名前衝突を心配するなら <FMDB/FMDatabase.h> のように記述してもよい。

SQL 文の定義

SQL 文は NSString リテラルとして定義する。FMDB の API 呼び出しで直に書くよりも、定数にしておいたほうが管理しやすいと思われる。この辺の話は NSString 連結を利用して heredoc 風に定数を記述するにまとめた。

定数の位置は DAO クラスの *.m ファイル冒頭にしておく。SQL 文が長く大量にある場合は別ファイルに括りだすのものよいだろう。

CRUD

FMDB API の代表的なものとして FMDatabase はデータベース接続と SQL 文の実行を担当して FMResultSet が処理結果となる。基本、これらだけ覚えれば利用できる。

CREATE、INSERT ( UPDATE )、DELETE には FMDatabase - executeUpdate メソッドを使用。これは第一引数に SQL 文となる NSString、それ以降は可変長引数になっていて SQL 文の Placeholder へ対応する。INSERT だとこんな感じ。

static NSString * const kSQLInsert = @""
"INSERT INTO "
  "books (author, title, release_date) "
"VALUES "
  "(?, ?, ?);";

- (Book *)add:(NSString *)author title:(NSString *)title releaseDate:(NSDate *)releaseDate {
    Book *book = nil;
    if ([self.db executeUpdate:kSQLInsert, author, title, releaseDate]) {
        NSInteger bookId = [self.db lastInsertRowId];
        book = [Book bookWithId:bookId author:author title:title releaseDate:releaseDate];
    }

    return book;
}

SELECT では FMDatabase - executeQuery メソッドを利用する。引数の仕様は executeUpdate と一緒。WHERE 句で条件指定する場合は第二引数以降も使用することになるだろう。以下は単純な全行取得の例。

static NSString * const kSQLSelect = @""
"SELECT "
  "id, author, title, release_date "
"FROM "
  "books;"
"ORDER BY "
  "author, title;";

- (NSArray *)read {
    NSMutableArray *books = [NSMutableArray arrayWithCapacity:0];
    FMResultSet    *results = [self.db executeQuery:kSQLSelect];

    while ([results next]) {
        [books addObject:[Book bookWithId:[results intForColumnIndex:0]
                                   author:[results stringForColumnIndex:1]
                                    title:[results stringForColumnIndex:2]
                              releaseDate:[results dateForColumnIndex:3]]];
    }

    return books;
}

戻り値は FMResultSet になる。これはカーソル型のオブジェクトで next メソッドを呼ぶことで取得された行カーソルが進む。next は成否を BOOL で返すため行の列挙は while で処理するとよい。

行カーソルが示す先のデータを取得するのも FMResultSet のメソッドになる。XXXXForColumnIndex 系が SELECT 文に指定された列のインデックス、XXXXForColumn 系は列名を指定して値を取得する。XXXX には返される値の型を示す。

インデックスと名前のどちらで取得するかはお好みで。インデックスは単純だが順番変更に弱い。名前の場合は管理が面倒だが、名前さえ維持できれば順番を意識せずに取得できる。

Swift から FMDB を利用する

Swift でフレームワーク内のクラスを利用する場合は名前空間を import するだけでよい。

import FMDB

非常に楽ちんだ。

SQL 文の定義

Swift でも Objective-C のようにクラスの外周に定数を宣言できるのだが、SQL 文と DAO の関係は密なのでクラス単位の static 定数としておく。

class BookDAO: NSObject {
    private static let SQLCreate = "" +
    "CREATE TABLE IF NOT EXISTS books (" +
      "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
      "author TEXT, " +
      "title TEXT, " +
      "release_date INTEGER" +
    ");"

    func create() {
        self.db.executeUpdate(BookDAO.SQLCreate, withArgumentsIn: nil)
    }
}

Swift の String は + で連結されるため、これを利用してインデントをつけている。

CRUD

FMDB API は Objective-C とほぼ共通。

CREATE、INSERT ( UPDATE )、DELETE には FMDatabase.executeUpdate(sql:, withArgumentsIn:) メソッドなどを使用する。第一引数に SQL 文となる String、第二引数の Array が SQL 文の Placeholder に対応づけられる。INSERT だとこんな感じ。

class BookDAO: NSObject {
    private static let SQLInsert = "" +
    "INSERT INTO " +
      "books (author, title, release_date) " +
    "VALUES " +
      "(?, ?, ?);"

    func add(author: String, title: String, releaseDate: Date) -> Book? {
        var book: Book? = nil
        if self.db.executeUpdate(BookDAO.SQLInsert, withArgumentsIn: [author, title, releaseDate]) {
            let bookId = db.lastInsertRowId()
            book = Book(bookId: Int(bookId), author: author, title: title, releaseDate: releaseDate)
        }

        return book
    }
}

SELECT は FMDatabase.executeQuery(sql:, withArgumentsIn:) メソッドとなる。引数の仕様は executeUpdate と一緒。以下は単純な全取得の例。

class BookDAO: NSObject {
    private static let SQLSelect = "" +
    "SELECT " +
      "id, author, title, release_date " +
    "FROM " +
      "books;" +
    "ORDER BY " +
      "author, title;"

    func read() -> Array<Book> {
        var books = Array<Book>()
        if let results = self.db.executeQuery(BookDAO.SQLSelect, withArgumentsIn: nil) {
            while results.next() {
                let book = Book(bookId: results.long(forColumnIndex: 0),
                                author: results.string(forColumnIndex: 1),
                                title: results.string(forColumnIndex: 2),
                                releaseDate: results.date(forColumnIndex: 3))
                books.append(book)
            }
        }

        return books
    }
}

Objective-C に対する特徴的な違いとして Optional 型の扱いがある。

Objective-C の場合、nil なオブジェクトに対し [obj message] 形式でメソッドやプロパティを呼び出すと処理が空振りする。これを利用して nil チェックを避けるテクニックがあるのだが、Swift なら if let で Optional な変数を受ければ配下のスコープで nil 済みの安全なオブジェクトを使用できる。

Objective-C の処理をそのまま Swift へ書き換えただけだと大量のエラーに見舞われて面食らうだろう。しかし慣れてくるとそれらが nil という曖昧な状態を避けるための設計ギプスとして有効であることを理解できる。unwrap まわりの面倒くささは確実に nil への抑止力となるはず。

こうした null/nil 安全については以下に詳しい。

Swift に触れ、私も null/nil 安全について以前より強く意識するようになった。

ユニット テスト

Xcode 標準のユニット テストを試す。

まず FMDB 関連は直に使用しない。DAO クラスのインスタンスは DAO Factory から得られるため、これらに関するものだけに依存する。

Xcode プロジェクトを作成する際に Include Unit Tests と UI Tests をチェックするとテスト用プロジェクトも追加される。前者がユニット テスト、後者は UI の動作をテストするものである。今回はユニット テストだけ使用した。

SQLite のデータベースはファイル単位として管理される。そのためテストを実行する都度、ファイルの存在をチェックして前回実行の結果に影響されぬよう注意が必要。もっと厳密にやるならテスト メソッド単位でファイルを消去するほうが安全だけど、そこまではしない。

Xcode のテストは Objective-C、Swift 共に XCTestCase を継承したテスト クラスを実装する。テスト全体の開始に setUp、終了時は tearDown が呼び出されるため、データベースの生成と破棄や DAO Factory 生成などはこれらに絡めておこなう。

以下は Swift の例。

import XCTest

class BookDAOTests: XCTestCase {
    private let filePath = BookDAOTests.databaseFilePath()
    private var daoFactory: DAOFactory!

    override func setUp() {
        super.setUp()

        self.clean()

        self.daoFactory = DAOFactory(filePath: self.filePath)
        if let dao = self.daoFactory.bookDAO() {
            dao.create()
        }
    }

    override func tearDown() {
        self.clean()
        super.tearDown()
    }

    func clean() {
        let manager = FileManager.default
        if manager.fileExists(atPath: self.filePath) {
            do {
                try manager.removeItem(atPath: self.filePath)
            } catch {
                print("Error: faild to remove database file.")
            }
        }
    }

    private static func databaseFilePath() -> String {
        let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
        let dir   = paths[0] as NSString
        return dir.appendingPathComponent("test.db")
    }
}

テスト対象にしたいクラスは Xcode 右にあるファイルのプロパティから Target Membership で Tests 系プロジェクトもチェックに含める必要あり。Swift は *.swift、Objective-C なら *.m ファイルをチェックすればよい。

Objective-C の場合、更に XCTestCase 派生クラス側のコードでテスト対象クラスのヘッダーを import しなければならない。ヘッダー管理の必要な言語って面倒だ。

これらの設定をせずにテストを実行しようとすると Tests プロジェクトのビルドでリンク エラーになる。Target Membership はよく忘れるので注意すること。

XCTestCase 派生クラスに testXXXX というメソッドを実装するとそれがテスト対象となる。テストを実行した時の処理順は以下。

  1. setUp
  2. testXXXX
  3. tearDown

実際に DAO クラスのデータ更新処理をテストしてみる。

class BookDAOTests: XCTestCase {
    func testUpdate() {
        if let dao = self.daoFactory.bookDAO() {
            let book = dao.add(author: "author", title: "title", releaseDate: Date())
            XCTAssertNotNil(book)

            // Before
            var books = dao.read()
            XCTAssertEqual(books[0].title, "title")

            // After
            let book2 = Book(bookId: (book?.bookId)!, author: (book?.author)!, title: "title2", releaseDate: (book?.releaseDate)!)
            XCTAssert(dao.update(book: book2))
            books = dao.read()
            XCTAssertEqual(books[0].title, "title2")

            XCTAssert(dao.remove(bookId: (book?.bookId)!))

        } else {
            XCTAssert(false)
        }
    }
}

テストにおける値の妥当性チェックは XCTAssert 系の関数で実施する。値の性質ごとに関数が提供されているので、適切なものを選ぼう。よく使うものとしては以下がある。

関数 機能
XCTAssert 一つの値に対して真であることを判定。偽ならば失敗する。
XCTAssertEqual 二つの値を比較。不一致ならば失敗し、それぞれの値の内容を表示する。Objective-C の場合、NSObject 系の比較には利用できないので注意する。Swift は問題なし。
XCTAssertEqualObjects 二つの値を比較。不一致なら失敗し、それぞれの値の内容を表示する。Objective-C で NSObject 系を比較する場合はこの関数を使用する。Swift は不要なので提供されていないようだ。
XCTAssertNil 一つの値に対して nil であることを判定。nil でなければ失敗する。
Not 系 各種 XCTAssert 関数名に Not のついたもの。例えば XCTAssertNotEqual など。元と判定が逆転する。

NSString を判定する場合、厳密さを意識する必要があるかもしれない。詳しくは以下を参照のこと。

Xcode 8.1 でもこの挙動はそのままである。例にある文字列をテストすると XCTAssertEqualObjects でも不一致となった。ここまで判定したいなら NSString - compare を使用する。

これが問題になるとしたら macOS の HFS+ みたいに Unicode 正規化で「が」を「か」と濁点にわけて管理しているものと、そうでない環境で得られたファイル名やパスの同一性などが考えられる。

データを扱う環境がひとつの系で完結しているとか文字エンコーディングと正規化の系が統一されていればよく、iOS アプリの場合はあまり意識しなくてよさそう。そのため今回のサンプルにおけるテストでは XCTAssertEqualObjects を採用した。

この問題が気になるなら以下の記事も参照のこと。

まとめ

Objective-C と Swift について。

Objective-C については 2 年前まで使用していたのでそれほど変化を感じない。元記事のサンプルと比較したら差分は大きいのだけど、想定どおりである。

一方、Swift は印象的だった。Swift 1 〜 3 までの変遷と混乱をチラ見して大変そうな印象しかなかったのだけど、実際にプログラミングしてみると nil 安全まわりが実によい。

基本的に nil を抑止する方針であること、使わざるを得ないときは Optional 型として明示される点が気に入っている。これを経験したことにより、null/nil 安全のない言語で書くときの設計も影響を受けるだろう。

ユニット テストについて。

他のプラットフォームにおける Power Assert 系のように XCTAssert も普通の比較でも失敗時に値の詳細を表示してほしい。用途ごとに関数を使い分けるのは面倒だ。いちおう Swift には keygx/PAssert があるのだけど、Power Assert 的なものは標準にしてもよいのではないか。

あと Cocoa Touch を使用するから仕方ないことだけど、テストに iOS シミュレーターや実機を要するため実行が遅い。スクリプト言語の気軽なテストに慣れていると重く感じる。

FMDB について。

細かな改善はたくさん反映されているのだろうけど、使用感は 5 年前と変わらず。これはよいことだ。普通に設計したらこうなるよね、という期待に答える直感的な API である。

以前よりサンプルの古さが気になっていたので、書き直せてスッキリした。

以下、余談。

この記事とサンプルは 2016 年末に書き終えていたのだけど Swift コードを構文強調する手段に WP Code Highlight.js を使うためブログ全体の Markdown 移行をしているうちに 2017 年へずれ込んでしまった。

結局 WP Code Highlight.js は行間へ余計な br タグを自動挿入するのを防げなくて導入は見送った。仕方ないので SyntaxHighlighter Evolved: Swift Brush を導入して構文強調している。

CocoaPods を試す

2016年12月19日 0 開発 , , ,

これまで akabekobeko/Examples-iOS というリポジトリで iOS に関するサンプルを公開していたのだが、以下のルールで新リポジトリ移行することにした。

  • サンプルごとにリポジトリを分割
    • Examples-iOS/VideoPlayer は examples-ios-video-player のように分割
    • 更新管理しやすくなる
  • 外部ライブラリは CocoaPods.org で管理する
    • 静的に組み込むのではなく、パッケージ管理に委ねる
    • ついでに FMDB サンプルを書き直して examples に追加したい
  • 可能なら Objective-C と Swift のサンプルを同時公開したい
    • iOS 以外でも Swift の利用が進みそうなので、触れておきたい
    • 慣れ親しんだ Objective-C で作成したものと同じサンプルを Swift 移植することで、学びやすくなるのではないか?
    • ひとつのサンプルに objc/swift というサブ ディレクトリがあり、そこが Xcode プロジェクトになる感じ

iOS アプリ開発から 2 年ぐらい遠ざかっておりサンプルも古びている。いまでは動かないものもあるだろう。これは、それらの棚卸しも兼ねた試みでもある。

もう Examples-iOS を更新することはないけれど、これはそのまま残しておく。かつてはこう書いていたという記録にはなるだろう。ただし更新されないサンプルは検索ノイズになってしまうから開発が中止されたことや代替サンプルについてだけ README で言及する予定。

CocoaPods

FMDB の解説記事は 2011 年に書かれたものだが現在も結構なアクセスがある。しかし ARC すら使われていない時代のもので、しかも組み込まれている FMDB が古すぎることから、いつか更新しなければと考えていた。

また、現在の iOS ライブラリ管理には CocoaPods を使用するのが一般的だろう。パッケージ管理にしておけば、ライブラリの依存やバージョン更新も楽になる。というわけで CocoaPods に入門する。

まずは CocoaPods をインストール。と、ここでいきなりつまづいた。

$ sudo gem install cocoapods
...
ERROR:  While executing gem ... (Errno::EPERM)
    Operation not permitted - /usr/bin/xcodeproj

私の環境は macOS Sierra なので、rootless により /usr への書き込みが制限されている。これは sudo でもはねられる。CocoaPods 公式の Getting Started を読むと --user-install でユーザー単位にインストールして shell の .profile にパスを通す方法を紹介していたので、試しにこれを採用してみた。

しかしこの方法でインストールした CocoaPods で生成した Podfile を Git リポジトリに入れて複数マシンで開発していたところ、新規に clone したプロジェクトに対して pod install した後にビルドしたら Podfile.lock No such file or directory というエラーが出るようになった。この問題は

などに解説されている方法で直るそうだが、私の環境ではダメだった。そもそも pod install の内容をみると Abort trap: 6 と出ている。これについて調べたら

にて CocoaPods の再インストールが勧められている。どうやら環境が壊れているらしい。そこでまずはアンインストール。

$ sudo gem uninstall cocoapods
Password:

Select gem to uninstall:
 1. cocoapods-0.34.2
 2. cocoapods-0.39.0
 3. All versions
> 3
Successfully uninstalled cocoapods-0.34.2
Remove executables:
    pod, sandbox-pod

in addition to the gem? [Yn]  Y
Removing pod
Removing sandbox-pod
Successfully uninstalled cocoapods-0.39.0

gem が複数バージョンあるようなので All versions を選び、すべて消した。その後に

$ sudo gem install -n /usr/local/bin cocoapods
...中略...
11 gems installed

でインストール。再び --user-install で入れるか迷ったが permission の通る場所ならばよいわけだし、いちいち .bash_profile を編集するのは面倒なのでこれでゆく。ちゃんとインストールできたか確認。

$ pod --version
1.1.1
$ gem list
...中略...
cocoapods (1.1.1)
cocoapods-core (1.1.1, 0.39.0, 0.34.2)
cocoapods-deintegrate (1.0.1)
cocoapods-downloader (1.1.3, 1.1.2, 0.9.3, 0.7.2)
cocoapods-plugins (1.0.0, 0.4.2, 0.3.1)
cocoapods-search (1.0.0, 0.1.0)
cocoapods-stats (1.0.0, 0.6.2)
cocoapods-trunk (1.1.2, 1.1.1, 0.6.4, 0.3.0)
cocoapods-try (1.1.0, 0.5.1, 0.4.1)

最新版が入ってる。CocoaPods 系 gem も単一バージョンだけのようだ。最後に CocoaPods を初期化する。

$ pod setup
Setting up CocoaPods master repo

CocoaPods 1.2.0.beta.1 is available.
To update use: `sudo gem install cocoapods --pre`
[!] This is a test version we'd love you to try.

For more information, see https://blog.cocoapods.org and the CHANGELOG for this version at https://github.com/CocoaPods/CocoaPods/releases/tag/1.2.0.beta.1

Setup completed

これで準備完了。

CocoaPods でライブラリをインストールする

ある Xcode プロジェクトに CocoaPods 経由でライブラリをインストールするための手順。CocoaPods が導入済みで pod setup まで完了していることを前提とする。

はじめに Terminal で Xcode プロジェクトのルートへ移動。これは 名.xcodeproj ファイルの置かれた場所になる。

次に Terminal から pod init を実行。これで Podfile が生成される。このファイルは YAML 形式となっており、Xcode プロジェクト単位で設定が階層化されている。Xcode プロジェクトを作成する際、テストを有効にした場合は Tests や UITests といったプロジェクトも生成されるので、以下のようになるはず。

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'UsingFMDB-Objective-C' do
  # Uncomment the next line if you're using Swift or would like to use dynamic frameworks
  # use_frameworks!

  # Pods for UsingFMDB-Objective-C
  pod 'FMDB/FTS'

  target 'UsingFMDB-Objective-CTests' do
    inherit! :search_paths
    # Pods for testing
  end

  target 'UsingFMDB-Objective-CUITests' do
    inherit! :search_paths
    # Pods for testing
  end

end

メインとなるプロジェクトでライブラリを使用したいならルートに追加する。この例では FMDB/FTS を追加している。

Swift プロジェクトの Podfile では use_frameworks! のコメント アウトを解除すること。こうしないと Swift 用のライブラリがインストールされないので注意する。

設定を済ませた後に pod instal を実行すると

$ pod install
Analyzing dependencies
Downloading dependencies
Installing FMDB (2.6.2)
Generating Pods project
Integrating client project
Sending stats
Pod installation complete! There is 1 dependency from the Podfile and 1 total pod installed.

こんな感じでライブラリがインストールされる。このとき .xcodeproj と同じ階層に以下のファイルとフォルダが追加される。

File/Folder 内容
.xcworkspace プロジェクト間の参照関係を管理するファイル。
Podfile.lock ライブラリのバージョンや依存情報を管理するためのファイル。
Pods/ インストールされたライブラリの実体を格納するフォルダ。

以降の開発で Xcode から開くファイルは .xcworkspace になる。間違って xcodeproj を対象にするとライブラリを参照できずビルドが通らないので気をつけること。

プロジェクトを Git リポジトリで管理しているなら gitignore/Objective-C.gitignore を使用していることだろう。CocoaPods を使用する場合、この .gitignorePods/ がコメントアウトされているので以下のように解除しておく。

# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
Pods/

このフォルダは動的に取得したライブラリを格納するのでリポジトリに含める必要はない。それ以外はコミットしておく。他の環境でプロジェクトと共に CocoaPods 関連を取得して pod install すれば同じ環境が整う。

CocoaPods でインストールしたライブラリの参照方法について。Objective-C の場合、

#import <FMDatabase.h>

のようにする。.xcworkspace を開いていれば、ライブラリの参照が通っているので Xcode エディタから補完されるはず。Swift の場合は

import FMDB

となる。なお、補完に出てこない場合は Xcode を開き直すとか、Product – Clean してから Product – Build すると直ることがある。

というか Swift はバックグラウンドで走る構文チェックが不安定で、エラーの原因を修正してもなかなか警告が消えないとか地味にストレスたまる。Swift 3 で言語の破壊的な変更は落ち着くそうだから、Xcode 9 あたりで Objective-C なみにサクサク開発できるようになるのかな、と期待している。

あとこれは愚痴なのだけど、Xcode や CocoaPods 関連は環境的な問題にしばしば遭遇する。CocoaPods は Xcode に後付でパッケージ管理を加えているので仕方のないことではあるが、プログラミングと異なり環境系の問題は操作基盤そのものなので原因を特定するのが難しい。

Stack Overflow などで対策を見つけても、それが起きる原理まで掘り下げていることは滅多にない。そのため指定された手順をなぞっても直らないとか、仕方ないから環境をクリーンにしてから再試行で直ったとかで釈然としない。こういうのを繰り返してるとだんだん堪忍袋が温まってくる。

あのプロプライエタリで有名だった Visual Studio ですら方針転換して NuGet を導入したのだから、Xcode もそろそろ公式に CocoaPods なりそれに類するパッケージ管理をサポートしてほしいものだ。

今後の予定

現在、FMDB サンプルを作成中。

Objective-C 版は完了したのでそれをベースに Swift 版へ処理を移植中。Swift 学習も兼ねているため小さなサンプルなのに苦戦している。

他の examples-ios-XXXX への移行は断続的におこなう。ひとつプロジェクトを作成するたびにそこで起きたことなどをブログに記録してゆく予定。