Closed eraydiler closed 4 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 ListMonitor
s 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.
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)
}
}
}
}
}
@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.
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.
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)
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):
var oldArray: [PostDetail]
from your existing message objects before you do any importsdiff(old: oldArray, new: source.details)
(still before importing)self.details
into a temporary var temp: [PostDetailCD] = []
changes
and work with the temp
array, not self.details
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.
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.
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.
Thanks for the answers @JohnEstropia, @tosbaha. After having some time I decided to go without using differenceKit.
This is quite old but to anyone following the thread, CoreStore now has ListPublisher
s which work in tandem with DiffableDataSource
s. This uses the same algorithm that DifferenceKit uses, so check the README for more info. Closing this thread now
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?