JohnEstropia / CoreStore

Unleashing the real power of Core Data with the elegance and safety of Swift
MIT License
3.99k stars 255 forks source link

CoreStore with differenceKit #284

Closed eraydiler closed 4 years ago

eraydiler commented 5 years ago

Hello,

I'm trying to use DifferenceKit library for updating UI triggered by relevant CoreStore methods. I simply need to have two different collection (before-update, after-update) to use these library.

However, it looks like the dataProvider.objects, which holds elements represented in UI, is being automatically updated, so I am unable to make a comparison between previous and updated data.

Is there a way to access the previous state of these objects so that I can make a comparison with the updated objects?

extension SimpleListViewController {
    func listMonitor(
        _ monitor: ListMonitor<LocalTask>,
        didUpdateObject object: LocalTask,
        atIndexPath indexPath: IndexPath) {

        guard
            let oldObjects = dataProvider.objects as? [LocalTask],
            var newObjects = dataProvider.objects as? [LocalTask]

            else {
                return
        }

        newObjects[indexPath.item] = object // These line is useless now, because newObjects is already updated

        let changeSet = StagedChangeset(
            source: oldObjects,
            target: newObjects
        )

        listView.reload(using: changeSet) { data in
            update(objects: data)
        }
    }
}
JohnEstropia commented 5 years ago

ListMonitor (or strictly speaking, NSFetchedResultsController) manages these changes for you so they are not really a good fit for DifferenceKit. If you run DifferenceKit on top of ListMonitors you will be unnecessarily duplicating a lot of heavy work.

Diff'ing libraries like DifferenceKit were designed for objects that don't manage states, meaning you have an "old version" and a "new version". With ORMs like Core Data, objects are live and update themselves in real time.

tosbaha commented 5 years ago

I have coded something before maybe it can help you too. If you are downloading data from a web server and want to compare the values, you may use a DifferenceKit. I personally use DeepDiff

So let's say you have new Data coming from a web server Which is called Post and each Post have some PostDetail 1:M Relationship

Storage.stack.rx.perform(asynchronous: { transaction in
// Import uniqueObjects
let imported = try transaction.importUniqueObjects(Into(PostCD.self), sourceArray: posts)
// Optional Remove non existent data from the local database
transaction.deleteAll(From<PostCD>(), Where<PostCD>("NOT (SELF IN %@)", imported))
})

In order for this code to work, your NSManagedObject classes must conform to ImportableObject protocol.


import CoreStore
import DeepDiff

extension PostDetailCD:ImportableObject {
    typealias ImportSource = PostDetail

    func didInsert(from source: PostDetail, in transaction: BaseDataTransaction) throws {
        self.message = source.message
        self.date = source.date
    }
}

extension PostCD:ImportableUniqueObject {
    typealias ImportSource = Post // Your Model from the Server
    // MARK: ImportableUniqueObject
    typealias UniqueIDType = String
    static var uniqueIDKeyPath: String {
        return #keyPath(PostCD.trackingID)
    }

    // MARK: ImportableObject
    static func shouldInsert(from source: ImportSource, in transaction: BaseDataTransaction) -> Bool {
        return true
    }

    static func shouldUpdate(from source: ImportSource, in transaction: BaseDataTransaction) -> Bool {
        return true
    }

    static func uniqueID(from source: Post, in transaction: BaseDataTransaction) throws -> String? {
        return source.trackingID
    }

    func update(from source: Post, in transaction: BaseDataTransaction) throws {
        self.name = source.name
        self.trackingID = source.trackingID

        if let oldDetails = self.detail.array as? [PostDetailCD] {
            var oldArray = [PostDetail]()
            for detail in oldDetails {
                oldArray.append(PostDetail.init(message: detail.message, date: detail.date))
            }

            let changes = diff(old: oldArray, new: source.details)
//            if changes.count > 0 {
//                print("Difference: \(changes)")
//            }

            for change in changes {
                switch change {
                case .delete(let detail):
                    transaction.delete(self.detail.object(at: detail.index) as? PostDetailCD)
                case .insert(let detail):
                    if let newDetail = try transaction.importObject(Into(PostDetailCD.self), source: detail.item) {
                        newDetail.parent = self
                        self.detail.insert(newDetail, at: detail.index)
                    }
                case .replace(let detail):
                    if let oldDetail = self.detail.object(at: detail.index) as? PostDetailCD {
                        oldDetail.message = detail.newItem.message
                        oldDetail.date = detail.newItem.date

                    } else {
                        print("This shouldn't happen!!")
                    }
                case .move(let detail):
                    self.detail.moveObjects(at: IndexSet(integer: detail.fromIndex), to: detail.toIndex)
                }
            }

        }
    }
}
JohnEstropia commented 5 years ago

@tosbaha I may be missing something your implementation, but will .replace be called if there are just updates to the PostDetail? The way I see it is by the time you call PosDetail.init, the detail instance would be the updated instance. (Unless you are managing the update order as well)

Also, since you are doing this in the context of the transaction, you would need extra handling if there was an error further on.

tosbaha commented 5 years ago

I haven't tested all possible cases but here is how it works. DeepDiff compares both Post and array PostDetail according to DeepDiff report it either deletes,insert or replace the item from the array. So if PostDetail array has 100 items and only one is added later on, I only insert once.Likewise, if 99 details are same but last detail is changed, I only replace the last item. Therefore I prevent heavy delete insert operations. As I said, I could be missing something but so far it works faster then deleting/replacing all details when the data on server changes. I will appreciate if you tell me if there is any mistake in my logic or implementation. Thanks.

tosbaha commented 5 years ago

I spoke too soon :( My code only works if last detail is changed since my insert code is buggy. It is not possible to insert at given index.

    self.detail.insert(newDetail, at: detail.index)

Above code doesn't seem to have any effect. It always inserts at the last when I use try transaction.importObject How can I solve this issue?

Only hacky way I have found is this

Since object is inserted at the end of Array, I move the object from end to whatever the index it is. self.detail.moveObjects(at: IndexSet(integer: self.detail.count - 1 ), to: index)

JohnEstropia commented 5 years ago

Assuming self.detail is a to-many relationship, and PostCD an NSManagedObject subclass, you cannot directly mutate the NSOrderedSet or NSSet property. See https://stackoverflow.com/a/27888302/809614

My suggestion for your case (if I understood the intent correctly):

  1. Create your var oldArray: [PostDetail] from your existing message objects before you do any imports
  2. Load the changes by calling diff(old: oldArray, new: source.details) (still before importing)
  3. Copy self.details into a temporary var temp: [PostDetailCD] = []
  4. Handle all changes and work with the temp array, not self.details
  5. When you're done set self.details = NSOrderedSet(array: temp)

It is a lot faster to set a relationship all at once than mutating the internal collection for each iteration.

JohnEstropia commented 5 years ago

Just a note here, that @eraydiler and @tosbaha 's cases are totally different. @eraydiler is trying to use Core Data objects directly with within diff(), while @tosbaha 's is diff'ing only the ImportSource arrays. The latter may be necessary depending on the use case, but the former is something I would discourage.

tosbaha commented 5 years ago

Thanks a lot @JohnEstropia I decided to just check if there is difference or not. If different set all at once. Thanks once again for taking your time for explaining in detail.

eraydiler commented 5 years ago

Thanks for the answers @JohnEstropia, @tosbaha. After having some time I decided to go without using differenceKit.

JohnEstropia commented 4 years ago

This is quite old but to anyone following the thread, CoreStore now has ListPublishers which work in tandem with DiffableDataSources. This uses the same algorithm that DifferenceKit uses, so check the README for more info. Closing this thread now