iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 18
1

這篇文章我們會專心在處理 Core Data 的部分,會獨立一篇文章處理的原因,是因為我們更改了 QuickRecord 這個 Entity 的部分欄位,造成 Core Data 沒辦法自動幫我們做 Light Weight Migration,所以我們必須協助 Core Data 做新舊資料轉換。

調整 QuickRecord、新增 Transaction 資料表

Transaction 為收支記錄使用的資料表,其內容除了 QuickRecord 的部分欄位(金額、標籤等等),還有一些自己的欄位(地點、消費類型等等),因此我們可以新增一個基礎的 QuickRecordBase 物件,用來定義兩者通用的欄位,然後 QuickRecord 和 Transaction 都繼承 QuickRecordBase,如下:

class QuickRecordBase: NSManagedObject {
    @NSManaged var id: UUID
    @NSManaged var amount: NSDecimalNumber
    @NSManaged var audioUUID: UUID
    @NSManaged var createdAt: Date
    @NSManaged var tags: Set<String>
}
class QuickRecord: QuickRecordBase {
    func fetchRequest() -> NSFetchRequest<QuickRecord> {
        return NSFetchRequest<QuickRecord>(entityName: "QuickRecord")
    }

    @NSManaged var isProcessed: Bool
    @NSManaged var transaction: Transaction
}
@objc class TransactionType: NSObject, NSCoding {
    static let INCOME: TransactionType = TransactionType(rawValue: 0)
    static let EXPENSE: TransactionType = TransactionType(rawValue: 1)

    let rawValue: Int

    private init(rawValue: Int) {
        self.rawValue = rawValue
    }

    func encode(with aCoder: NSCoder) {
        aCoder.encode(rawValue, forKey: "rawValue")
    }

    required init?(coder aDecoder: NSCoder) {
        self.rawValue = aDecoder.decodeInteger(forKey: "rawValue")
    }
}

class Transaction: QuickRecordBase {
    func fetchRequest() -> NSFetchRequest<Transaction> {
        return NSFetchRequest<Transaction>(entityName: "Transaction")
    }

    @NSManaged var location: String
    @NSManaged var type: TransactionType
    @NSManaged var quickRecord: QuickRecord
}

因為我想在 Transaction 的 type 欄位存 enum,所以我用物件偽裝成 enum 的運作模式,企圖欺騙 Core Data,當然有了 NSCoding 加上 Transformable 的協助,Core Data 根本不是對手。

備註:這裡把 amount 從字串改成 Decimal 的原因,是因為我想偷渡這些 IssueIssue

蹦!爆炸了。

如上修改完後,因為 Core Data 沒辦法瞭解我們這次更動的欄位內容,它不知道如何去做新舊表的 Migration(底層還是 SQLite,沒有黑魔法),所以只要打開 APP 就會直接崩潰,因此我們需要進一步提供 Core Data 有關於我們這次資料表 Migration 的資料。

會看到崩潰訊息中,原因如下:

reason=Can't find or automatically infer mapping model for migration

就是 Core Data 跟你說你改了結構,Core Data 也沒辦法猜測你的目的,所以選擇讓程式崩潰,你得定義出新舊結構轉換的規則。

新增新版的 Model

$ tree Money\ Mom/Model/Model.xcdatamodeld/
Money\ Mom/Model/Model.xcdatamodeld/
├── Model\ 2.xcdatamodel
└── Model.xcdatamodel

2 directories, 0 files

此時系統中就會同時存在 Model 和 Model 2 兩種結構,我們可以在 Xcode 中指定當前版本為 Model 2,也就是第二版的 Model。

建立 Mapping Model 協助 Core Data 做資料轉換

指定 Model 2 後,只要 Core Data 開始運作時,它就會發現世界不一樣了,然後開始尋找系統中是否有參考資料,可以協助它做新舊結構轉換,這個參考資料就是「Mapping Model」。

因此我們需新增一個 Mapping Model,然後指定一個 NSEntityMigrationPolicy(如果只是簡單的轉換不一定要用 Policy),該 Policy 中就會定義如何從「舊的 Model」轉換成「新的 Model」,如下:

final class StringAmountToDecimalAmountPolicy: NSEntityMigrationPolicy {
    override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws {
        let dInstance = NSEntityDescription.insertNewObject(forEntityName: mapping.destinationEntityName!, into: manager.destinationContext)
        dInstance.setValue(sInstance.value(forKey: "id"), forKey: "id")
        dInstance.setValue(NSDecimalNumber(string: sInstance.value(forKey: "amount") as! String), forKey: "amount")
        dInstance.setValue(sInstance.value(forKey: "audioUUID"), forKey: "audioUUID")
        dInstance.setValue(sInstance.value(forKey: "created_at"), forKey: "createdAt")
        dInstance.setValue(sInstance.value(forKey: "tags"), forKey: "tags")
        dInstance.setValue(false, forKey: "isProcessed")
        dInstance.setValue(nil, forKey: "transaction")

        manager.associate(sourceInstance: sInstance, withDestinationInstance: dInstance, for: mapping)
    }
}

轉換的程式中,只要有任何地方有錯誤崩潰,轉換就會失敗,Core Data 會將資料還原成轉換前的樣子,所以我們應該放心且適當地使用「!」。

展示

你以為有 GIF 可以看嗎?抱歉,沒有。

程式碼

如何驗證轉換成功?打開 APP 發現原本資料還在就對了,因為轉換「失敗」只會有兩種結果:

  1. APP 崩潰
  2. 沒有顯示任何資料(因為 Core Data 開始使用新的 Model)

痛苦結束了,下一篇就可以繼續往前實做功能囉。


上一篇
Money Mom - 實做新增收支記錄的界面 Part 1
下一篇
Money Mom - 實做收支記錄的界面 Part 2
系列文
iOS 三十天上架記帳 APP30

1 則留言

0
Don
iT邦新手 5 級 ‧ 2018-01-06 09:44:35

可以放 coding 過程的 GIF lol~

讚讚

Don iT邦新手 5 級‧ 2018-01-06 18:24:41 檢舉

/images/emoticon/emoticon70.gif

/images/emoticon/emoticon08.gif

我要留言

立即登入留言