iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 18
2
Software Development

iOS 三十天上架記帳 APP系列 第 18

Money Mom - 實做新增收支記錄的界面 Part 1.5

這篇文章我們會專心在處理 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~

Andy Tsai iT邦新手 5 級 ‧ 2018-01-06 11:07:47 檢舉

讚讚

/images/emoticon/emoticon70.gif

/images/emoticon/emoticon08.gif

我要留言

立即登入留言