JavaScriptプログラマがSwift iOSアプリを2週間で作って公開してみた〜その9 CoreData〜


JavaScriptプログラマー(JSer)がSwiftデビューして、ただ作りたいアプリを作ってみたシリーズ第9回目です。

前回はUITableViewCellのスワイプ時のアクションを追加する方法を紹介しました。今回はアプリ内にデータをstoreするための仕組みであるCoreDataの紹介です。

そういえば先日ようやくTwitStockerがリリースされました!Appleへの申請を終えてから約1か月遅れです。
2回ほど些細な理由でrejectされて対応したりしてました。そこらへんも後日の記事で書きたいと思います。

ダウンロードはこちらから。
https://itunes.apple.com/en/app/twitstocker/id958798898?l=ja&ls=1&mt=8

アプリ内データについて

さて、このアプリの中で、左スワイプすると既読となり表示されなくなりますが、その既読データの管理はどのように行っているでしょうか。
サーバー側で管理するか、クライアント側で管理するか迷いましたが、今回はクライアント側でデータを持たせるようにしました。

それぞれメリットデメリットがあって、

今回のようにクライアント側で既読データを持つ場合のメリット

  • 通信が必要ないので処理が速い
  • サーバー側の実装が不要なので、コード量が少なく実現可能

一方デメリットもあります

  • 大きいデータはクライアント端末を圧迫する
  • アプリをアンインストールするとデータは消える

こんなところでしょうか、今回はアプリケーション内でデータを保存したりできる、CoreDataを使います。

ただし、デメリットに記述してある通り、データ量が膨大になるのは避けたいです。そこで、今回の実装では、Twitterの検索結果の取得期間がせいぜい2週間である、という性質を利用して、「約1ヶ月以上前の既読データはアプリ起動時に消す」という処理を入れています。そこらへんの日時の比較などについては第18回あたりで解説しようと思っています。

長い前置きとなりましたが、今回はそのCoreDataを導入する部分について紹介します。

データモデルの作成

xcodeで新規ファイル->CoreDataを選択します。

スクリーンショット 2015-02-15 11.08.32

作成すると、xxx.xcdatamodeldというファイルが作られると思います。
そこの下の方にある”Add Entry”というボタンを押して、データモデルのエントリを追加していきます。

既読データの管理のためのモデルを作るので、名前をReadとしておきます。(モデル名はEnterキーやダブルクリックで変更できます)

また、保存するデータの項目として「日時」と「TweetID」を持たせるために、createdAtとidというフィールドをAttributesに追加します。型もそれぞれセット。

スクリーンショット 2015-02-15 11.12.38

これでもモデルの定義はOKです。

SwiftでCoreDataを扱うためのAppDelegateの記述

本来はxcodeでプロジェクト作成時にCoreDataを使うと指定しておくと、自動的にAppDelegateにコードが追加されるようなのですが、後からデータモデルを足した場合は記述を追加する必要があります。
とりあえずおまじないだと思ってたしました。(一部application名やsqliteファイル名を変更する必要があります)

    // MARK: - Core Data stack
    
    lazy var applicationDocumentsDirectory: NSURL = {
        // The directory the application uses to store the Core Data store file. This code uses a directory named "tejitak.com.twitstocker" in the application's documents Application Support directory.
        let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
        return urls[urls.count-1] as NSURL
        }()
    
    lazy var managedObjectModel: NSManagedObjectModel = {
        // The managed object model for the application. This property is not optional. It is a fatal error for the application not to be able to find and load its model.
        let modelURL = NSBundle.mainBundle().URLForResource("twitstocker", withExtension: "momd")!
        return NSManagedObjectModel(contentsOfURL: modelURL)!
        }()
    
    lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator? = {
        // The persistent store coordinator for the application. This implementation creates and return a coordinator, having added the store for the application to it. This property is optional since there are legitimate error conditions that could cause the creation of the store to fail.
        // Create the coordinator and store
        var coordinator: NSPersistentStoreCoordinator? = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
        let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("twitstocker.sqlite")
        var error: NSError? = nil
        var failureReason = "There was an error creating or loading the application's saved data."
        if coordinator!.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: url, options: nil, error: &error) == nil {
            coordinator = nil
            // Report any error we got.
            var dict = [String: AnyObject]()
            dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data"
            dict[NSLocalizedFailureReasonErrorKey] = failureReason
            dict[NSUnderlyingErrorKey] = error
            error = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)
            // Replace this with code to handle the error appropriately.
            // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
            NSLog("Unresolved error \(error), \(error!.userInfo)")
            abort()
        }
        
        return coordinator
        }()
    
    lazy var managedObjectContext: NSManagedObjectContext? = {
        // Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.) This property is optional since there are legitimate error conditions that could cause the creation of the context to fail.
        let coordinator = self.persistentStoreCoordinator
        if coordinator == nil {
            return nil
        }
        var managedObjectContext = NSManagedObjectContext()
        managedObjectContext.persistentStoreCoordinator = coordinator
        return managedObjectContext
        }()
    
    // MARK: - Core Data Saving support
    
    func saveContext () {
        if let moc = self.managedObjectContext {
            var error: NSError? = nil
            if moc.hasChanges && !moc.save(&error) {
                // Replace this implementation with code to handle the error appropriately.
                // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                NSLog("Unresolved error \(error), \(error!.userInfo)")
                abort()
            }
        }
    }

DataModelに対応したシングルトンクラスの作成

次にアプリ内のコードでデータstoreを扱うために以下のようなシングルトンクラスを用意しました。

import Foundation
import UIKit
import CoreData

class ReadStore {
    
    let entityName:String = "Read"

    let expiration: Int = 30
    var readDataList = [NSManagedObject]()
    
    class var sharedInstance :ReadStore {
        struct Static {
            static let instance = ReadStore()
        }
        return Static.instance
    }

    // read from CoreData
    func load() {
        self.readDataList = []
        /* Get ManagedObjectContext from AppDelegate */
        let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
        let manageContext = appDelegate.managedObjectContext!
        /* Set search conditions */
        let fetchRequest = NSFetchRequest(entityName: self.entityName)
        var error: NSError?
        /* Get result array from ManagedObjectContext */
        let fetchResults = manageContext.executeFetchRequest(fetchRequest, error: &error)
        if let results: Array = fetchResults {
            // データがロードされた時の処理
            for obj:AnyObject in results {
                self.readDataList.append(obj as NSManagedObject)
            }
        }
    }


    // Add an entry already read in CoreData
    func saveReadData(id: String, createdAt: NSDate){
        /* Get ManagedObjectContext from AppDelegate */
        let appDelegate: AppDelegate = UIApplication.sharedApplication().delegate as AppDelegate
        let managedContext: NSManagedObjectContext = appDelegate.managedObjectContext!
        /* Create new ManagedObject */
        let entity = NSEntityDescription.entityForName(self.entityName, inManagedObjectContext: managedContext)
        let obj = NSManagedObject(entity: entity!, insertIntoManagedObjectContext: managedContext)
        /* Set the name attribute using key-value coding */
        obj.setValue(id, forKey: "id")
        obj.setValue(createdAt, forKey: "createdAt")
        /* Error handling */
        var error: NSError?
        if !managedContext.save(&error) {
//            println("Could not save \(error), \(error?.userInfo)")
        }
    }
}

とりあえず、loadとsaveする処理を用意しました。
loadでは、NSFetchRequestというものを作成して、managedObjectContextのexecuteFetchRequestに渡してデータを取得します。

save時には、NSManagedObjectというオブジェクトにinsertIntoManagedObjectContextを指定して保存します。

起動時に以下のコードを実行するとアプリ内に保存しているデータがロードされます。

ReadStore.sharedInstance.load()

ユーザーが既読アクションを実行した時はそのデータを保存したいので、以下のようにsaveReadDataを呼び出します

ReadStore.sharedInstance.saveReadData(tweet.tweetID, createdAt: tweet.createdAt)

各NSManagedObjectのAttributeデータを取り出したい時はfetchしたresultのデータからvalueForKeyを使ってキャストして取得できます。

let id:String? = obj.valueForKey("id") as? String
let createdAt:NSDate? = obj.valueForKey("createdAt") as? NSDate

こんな感じでアプリ内のデータの保存・取得ができます。だんだんアプリっぽくなってきましたね!
次回はUIの部品の話に戻って、 引っ張ってロードするよくあるやつ(UIRefreshControl)について紹介します。

TwitStockerのダウンロードはこちらから。
https://itunes.apple.com/en/app/twitstocker/id958798898?l=ja&ls=1&mt=8