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風なアプリを作ってみることにします。
サンプルコードはこちら
https://github.com/eversense/SwiftData-Example
下記でチュートリアルとして解説していきます。
##準備
###プロジェクト立ち上げ
Xcodeから File -> New -> Project をクリック
Master-Detail Applicationを選びます。
適当な名前で保存してください。私はSwiftData-Exampleとしました。
この状態でシュミレーターで起動してみると、tableViewのよくありそうなアプリが動きます。ただ初期状態ではデータを保持するようにはなっていません。そこでSwiftDataを使ってデータを保存するようにしていきます。
###SwiftDataのインストール
本家の説明にはgit submoduleでインストールするように書いてありますが、今回はサンプルなので、githubからZIPでダウンロードしたSwiftData.swiftをそのままプロジェクトにドラックして追加しました。
- Xcode 左ペインから、プロジェクトを選択します
- 上部のタブから Build Phases を選択
- Link Binary With Libraries を開く
- Link Binary With Libraries の左下の+ をクリック
- Choose framework and libraries add: というダイアログが表示されるので、「sqlite」と入力
- libsqlite3.dylib を選択
- Addをクリックします
libsqlite3.dylibはobjective-cで書かれているため、Briding-Header.hファイルを追加する必要があります。
- 左ペインのプロジェクト名のところで右クリック -> New File...を選択
- iOSのSourceからHeader Fileを選択し、Next
- 「Briding-Header.h」という名前でCreateします
- そのファイルに下記のように#import "sqlite3.h"を追加します。
Briding-Header.h
#ifndef SwiftData_Example_Briding_Header_h #define SwiftData_Example_Briding_Header_h #import "sqlite3.h" #endif
- 左ペインのプロジェクト名を選択し、Build Settingsのタブを開きます。
- BasicとAllの表示切替をAllにします
- 検索窓に「Objective-C Bridging Header」と入力します。
- Objective-C Bridging Headerの項目に「Briding-Header.h」と入力します。
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