iOS
Swift

[iOS8] SwiftData(Swift製SQLiteラッパー)を使ったMVCサンプル

More than 3 years have passed since last update.

Objective-C時代のSQLiteラッパーといえば、FMDBが定番でした。
SwiftでもObjective-cとの連携が可能なので、FMDBはそのまま利用可能ですが、せっかくなのでSwift製のSQLiteラッパーを探してみました。

SwiftData

幾つかみつかりましたが、人気がありそうなのがSwiftDataでした。
https://github.com/ryanfowler/SwiftData
直接SQLでかけることはもちろん、エラーハンドリングが簡単だったり、結果がArrayで返してくれたり、記述が少しで済むなど、とても優れているライブラリでしたのでご紹介したいと思います。

MVCっぽいアプリのサンプル

Xcodeで新しいiOSアプリケーションのプロジェクトを作るときに、いくつかのテンプレートの中にMaster-Detail Applicationというものがあると思います。このテンプレートにSwiftDataライブラリを追加し、Modelを作成してMVC風なアプリを作ってみることにします。

iOS Simulator Screen Shot 2014.10.08 21.31.43

サンプルコードはこちら
https://github.com/eversense/SwiftData-Example

下記でチュートリアルとして解説していきます。

準備

プロジェクト立ち上げ

Xcodeから File -> New -> Project をクリック
Master-Detail Applicationを選びます。

スクリーンショット 2014-10-08 19.37.38

適当な名前で保存してください。私はSwiftData-Exampleとしました。
この状態でシュミレーターで起動してみると、tableViewのよくありそうなアプリが動きます。ただ初期状態ではデータを保持するようにはなっていません。そこでSwiftDataを使ってデータを保存するようにしていきます。

SwiftDataのインストール

本家の説明にはgit submoduleでインストールするように書いてありますが、今回はサンプルなので、githubからZIPでダウンロードしたSwiftData.swiftをそのままプロジェクトにドラックして追加しました。

スクリーンショット 2014-10-08 19.56.37

libsqlite3.dylibの追加

次に'libsqlite3.dylib'というライブラリを追加する必要があります。手順は以下のようです。


  1. Xcode 左ペインから、プロジェクトを選択します

  2. 上部のタブから Build Phases を選択

  3. Link Binary With Libraries を開く

  4. Link Binary With Libraries の左下の+ をクリック

  5. Choose framework and libraries add: というダイアログが表示されるので、「sqlite」と入力

  6. libsqlite3.dylib を選択

  7. Addをクリックします


スクリーンショット 2014-10-08 19.55.10

libsqlite3.dylibはobjective-cで書かれているため、Briding-Header.hファイルを追加する必要があります。


  1. 左ペインのプロジェクト名のところで右クリック -> New File...を選択
    スクリーンショット 2014-10-08 20.03.49

  2. iOSのSourceからHeader Fileを選択し、Next

  3. 「Briding-Header.h」という名前でCreateします

  4. そのファイルに下記のように#import "sqlite3.h"を追加します。
    Briding-Header.h
    #ifndef SwiftData_Example_Briding_Header_h
    #define SwiftData_Example_Briding_Header_h
    
    #import "sqlite3.h"
    
    #endif
    


  5. 左ペインのプロジェクト名を選択し、Build Settingsのタブを開きます。

  6. BasicとAllの表示切替をAllにします

  7. 検索窓に「Objective-C Bridging Header」と入力します。
    スクリーンショット 2014-10-08 20.09.40

  8. Objective-C Bridging Headerの項目に「Briding-Header.h」と入力します。
    スクリーンショット 2014-10-08 20.12.01


これでライブラリを使う準備ができました。

Modelの作成

データベースを操作するModelを作ります。SampleModel.swiftというswiftファイルを追加しました。

データベースの作成

データベースはデフォルトでSwiftData.sqliteというファイルが自動的に作られます。そのため何もする必要がありません。簡単ですね。

テーブルの作成

Modelの初期化のタイミングでテーブルを用意していきます。

import Foundation

class SampleModel {

    init() {
        let (tb, err) = SD.existingTables()
        if !contains(tb, "samples") {
            if let err = SD.createTable("samples", withColumnNamesAndTypes: ["data": .StringVal]) {
                //there was an error during this function, handle it here
            } else {
                //no error, the table was created successfully
            }
        }
        println(SD.databasePath())
    }
}

テーブルの存在確認をし、なければsamplesというテーブルを作成していきます。カラムとしてString型のdataカラムを追加します。
テーブルを作成すると自動的にIDカラムも追加されるので、ID, dataという2つのカラムのsamplesテーブルが作成されることになります。IDにはAUTOINCREMENTが設定され、レコードが追加されるたびに連番が入ります。
またインラインでエラーハンドリング出来る記述が可能です。SwiftDataの公式に書いてある記述を参考にしています。
また最後の行にprintln(SD.databasePath())と書いて、ログにデータベースのパスを表示させるようにしています。

SampleModelの呼び出し

メインのコントローラーである、MasterViewControllerでSampleModelを利用します。

import UIKit

class MasterViewController: UITableViewController {

    var detailViewController: DetailViewController? = nil
    var objects = NSMutableArray()
    let sampleModel = SampleModel() //SampleModelのオブジェクト作成

最初のコードの抜粋ですが、一番下の行(7行目)でSampleModelオブジェクトを作成しています。この状態で一度ビルドしてシュミレーターを起動してみると、ログにsqliteのpathが出力されます。Modelの初期化でデータベースのsqliteファイルが作成されて、テーブルが作られます。

データの追加

Model側:addメソッド

アプリのプラスボタンを押した時に日時が追加されますが、その情報を保存していくためにまずはモデル側のSampleModelに下記のようなaddメソッドを追加します。

    func add(data:NSDate) -> Int{
        var result: Int? = nil
        if let err = SD.executeChange("INSERT INTO samples (data) VALUES (?)", withArgs: [data]) {
            //there was an error during the insert, handle it here
        } else {
            //no error, the row was inserted successfully
            let (id, err) = SD.lastInsertedRowID()
            if err != nil {
                //err
            }else{
                //ok
                result = Int(id)
            }
        }
        return result!
    }

NSDate型で受け取ったデータを追加します。型を意識せずに保存できるのがいいですね。追加後はInsertしたidを返すようにしています。ちなみにこのコードでエラーが発生したらクラッシュしますが、サンプルなので省略しますね。

Controller側:sampleModel.add()を使って保存

プラスボタンを押した時にテーブルセルにデータが追加されます。元のソースには下記のように書いてあります。

    func insertNewObject(sender: AnyObject) {
        objects.insertObject(NSDate.date(), atIndex: 0)
        let indexPath = NSIndexPath(forRow: 0, inSection: 0)
        self.tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
    }

これを次のように改変して、追加すると同時にデータベースに保存出来るようにしていきます。

    func insertNewObject(sender: AnyObject) {
        let now = NSDate.date()
        let id = sampleModel.add(now)
        let dic = ["ID":id, "data":now]
        objects.insertObject(dic, atIndex: 0)
        let indexPath = NSIndexPath(forRow: 0, inSection: 0)
        self.tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
    }

元ソースではobjectsにNSDate型のオブジェクトをそのまま入れていましたが、あとでデータを削除するときにIDが必要になるので、Dictionary型でobjectsに入れておきます。このままシュミレーターを起動してもクラッシュします。Dictionary型を入れたので、取り出すほうも変更しなくてはいけません。

データの表示

objectsからデータを取り出す場合には

let object = objects[indexPath.row] as NSDate

のように記述されているところが「prepareForSegueメソッド」と、「tableViewのUITableViewCellを返すメソッド」の2ヶ所にあります。 objectsにはDictionary型を使うように変更したので下記のように書き換えます。

let object = objects[indexPath.row]["data"] as NSDate

この状態でシュミレーターを起動すると正しく動作します。 次に起動時にDBからデータを読み出す処理を作っていきましょう。

データの取得

Model側:Selectでデータを読み出す

今度はDBに保存したデータを起動時に読み込むようにしていきます。まずModel側の準備です。下記のようなgetAllメソッドを書きました。

    func getAll() -> NSMutableArray {
        var result = NSMutableArray()
                let (resultSet, err) = SD.executeQuery("SELECT * FROM samples ORDER BY ID DESC")
        let dateFormatter = NSDateFormatter()
        if err != nil {

        } else {
            for row in resultSet {
                if let id = row["ID"]?.asInt() {
                    let dataStr = row["data"]?.asString()!
                    dateFormatter.dateFormat = "YYYY/MM/dd HH:mm:ss"
                    let data = dateFormatter.dateFromString(dataStr!)
                    let dic = ["ID":id, "data":data!]
                    result.addObject(dic)
                }
            }
        }
        return result
    }

samplesテーブルの全件をID降順で取得します。その後、for inで回し、NSMutableArrayに["ID":id, "data":data]というDictionaryを入れて返しています。

Controller側:起動時、objectsにデータを追加

起動時に走るviewDidLoadに下記の一文加えるだけで、OKです。

objects = sampleModel.getAll()

MVCらしく簡潔な記述になりますね。 シュミレーターを起動してみると、これまで追加してきたデータが表示されるようになると思います。ここまででデータの追加、取得まで出来るようになりました。

 データの削除

このアプリではデータの削除機能もあるので、最後にそれも実装していきます。

Model側:deleteメソッド

idを元に、レコードを削除出来るように下記を記述します。

    func delete(id:Int) -> Bool {
        if let err = SD.executeChange("DELETE FROM samples WHERE ID = ?", withArgs: [id]) {
            //there was an error during the insert, handle it here
            return false
        } else {
            //no error, the row was inserted successfully
            return true
        }
    }

Controller側:deleteを追加する

MasterViewControllerの一番最後のメソッドに上記で用意したdeleteを使ってテーブルから削除をおこないます。

    override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
        if editingStyle == .Delete {
            sampleModel.delete(objects[indexPath.row]["ID"] as Int)
            objects.removeObjectAtIndex(indexPath.row)
            tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
        } else if editingStyle == .Insert {
            // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view.
        }
    }

4行目が追加された行です。 これで全ての機能がデータベースと連携することになりました。
以上、お疲れ様でした!

最後に

MVCのようにモデルを作ることで、このような機能追加でもコントローラーへの記述がわずかだったのがわかると思います。iOSでデータベースを利用する場合にはラッパーを使って簡単にDBを扱い、モデルを作成してコントローラーを簡潔にしたいですね!

こちらにサンプルソースコードを置いておきます。
https://github.com/eversense/SwiftData-Example