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


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

前回は設定パネルに設置したフォーム部品からデータを取り出しアプリ内で永続的に保存する例を紹介しました。今回は、保存したデータをNSDateを使った日付の比較をして消す処理について紹介します。

TwitStockerのどこで日付を扱った処理をしているのかと言うと、CoreDataを使用してアプリ内にストアしている「既読済みtweetIdリスト」を定期的にアプリ起動時に古いものを削除する部分で使用しています。既読として扱ったtweetIdを削除している理由は、Twitter Search APIで返せるtweetsはせいぜい2週間前までしか遡れないため削除しても問題ないという判断をしているからです。それによって、アプリ内の保存データのサイズが大きくなるのを防ぐことができます。

swiftでは現在日時など日付を扱うためにNSDateクラスが用意されています。TwitStockerではそのNSDateを使って、CoreDataとして保存されているデータを読みだし、expiration(今は30日間)として設定されている期間を超えたら以前のデータを削除する処理を実装しています。

既読データモデルの定義

まずは以下のように既読(Read)データをCoreDataのモデルを定義しています。

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

左スワイプをして各tweetが「既読」になった時に、内部的には既読データとしてtweetのIDとそのtweetの日時をペアで保存しています。それぞれ上記のモデルのidとcreatedAtに対応しています。createdAtのデータタイプはDateです。

既読データを保存する

先に、既読データエントリを追加する部分を紹介します。
以下のコードがtweetのcellをスワイプした時に行われる処理です。スワイプされたらトーストのポップアップで「既読にしました」と表示して、ReadStore.sharedInstance.saveReadDataの部分でデータを保存しています。そのあとの行ではタイトルに表示されている「未読記事(xx)」のカウントを減らし、tableViewからcellを一つ取り除いて再度tableを表示しています。

TimelineViewController.swift

    func submitRead(index: Int) {
        if tweets.count > index {
            // toast popup notification
            self.view.makeToast(message: NSLocalizedString("stock_alert_read_done", comment: ""), duration: 2, position: HRToastPositionTop)
            var tweet = self.tweets[index]
            // store to local storage
            ReadStore.sharedInstance.saveReadData(tweet.tweetID, createdAt: tweet.createdAt)
            // remove a cell from table
            self.tweets.removeAtIndex(index)
            // update title with new count
            self.unreadCount--
            self.updateTitle()
            self.tableView!.reloadData()
        }
    }

データを保存しているReadStore.sharedInstance.saveReadDataは以前の第9回目のCoreDataに関する記事で紹介しています。おさらいすると、以下のコードでidとcreatedAtを保存しています。

ReadStore.swift

    // 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")
    }

これで永続的にアプリ内にデータが保存されるようになりました。

日付比較をして既読データを削除する

次に、この既読データのロード時に保存されているデータの日付と現在時刻とを比較して、30日間のexpirationを過ぎているのものは削除していきます。
以下のloadのfuncの中のfor文の中にある部分で取り出したデータの日付と現在の時刻の比較を行っています。

ReadStore.swift


    let expiration: Int = 30
    var readDataList = [NSManagedObject]()

    // 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 {
            var removeList = [NSManagedObject]();
            for obj:AnyObject in results {
                // check if expiration past to delete old tweets never shown in search API
                // 保存されているデータの日付を取得してNSDateにキャスト
                let createdAt:NSDate? = obj.valueForKey("createdAt") as? NSDate
                // NSCalendarを使用して現在日時を取得
                let calendar = NSCalendar.currentCalendar()
                // 時間を扱いやすいクラスであるNSDateComponentsをnewする
                let comps = NSDateComponents()
                // expirationで指定している日数分だけマイナスする
                comps.day = -self.expiration
                // expirationされる30日前の日付をNSDateで取得する
                let expirationDate = calendar.dateByAddingComponents(comps, toDate: NSDate(), options: NSCalendarOptions.allZeros)
                // NSDate同士の比較。createdAtがexpirationDateよりも前だったら消すリストに追加する
                if createdAt?.compare(expirationDate!) == NSComparisonResult.OrderedAscending {
                    removeList.append(obj as NSManagedObject)
                    continue
                }
                self.readDataList.append(obj as NSManagedObject)
            }
            // remove
            for obj:NSManagedObject in removeList {
                self.deleteReadData(obj, reload: false)
            }
        }
    }

    // delete an entry in CoreData
    func deleteReadData(managedObject: NSManagedObject) {
        /* Get ManagedObjectContext from AppDelegate */
        let appDelegate: AppDelegate = UIApplication.sharedApplication().delegate as AppDelegate
        let managedContext: NSManagedObjectContext = appDelegate.managedObjectContext!
        /* Delete managedObject from managed context */
        managedContext.deleteObject(managedObject)
    }

コード内のコメントに書いてある部分が日付比較によって30日間経ったエントリを削除するという処理です。これで不必要な過去のデータがずっとアプリ内に蓄積することがなくなりました。

以上、今回はNSDateを使った日付比較によってCoreDataを使ったデータを削除する話でした。次回はチュートリアルのページの実装に入ります!

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