Skip to content

Instantly share code, notes, and snippets.

@tonnylitao
Last active April 14, 2021 12:46
Show Gist options
  • Save tonnylitao/00c7cb8efeebfa0e91ae333d2c3e6396 to your computer and use it in GitHub Desktop.
Save tonnylitao/00c7cb8efeebfa0e91ae333d2c3e6396 to your computer and use it in GitHub Desktop.
yet another way to sync core data with paging api
import Foundation
import CoreData
struct RemoteUser: Decodable {
var userId: Int
var name: String
}
class User: NSManagedObject {
@NSManaged public var userId: Int
@NSManaged public var name: String
func importFrom(_ object: RemoteUser) {
//get rid of context.hasChanges is true even set same value
if userId != object.userId { userId = object.userId }
if name != object.name { name = object.name }
}
}
class PagingDataSyncEngine {
var container: NSPersistentContainer!
private lazy var backgroundContext: NSManagedObjectContext = {
container.newBackgroundContext()
}()
func sync(from remoteUsers: [RemoteUser], offset: Int, isFullFilled: Bool, completion: @escaping (Error?) -> ()) {
let moc = backgroundContext
moc.perform {
var err: Error?
do {
if remoteUsers.isEmpty {
try self.batchDeleteUsers { fetchRequest in
fetchRequest.fetchOffset = offset
}
}else {
try self.sync(from: remoteUsers, isFullFilled: isFullFilled)
if moc.hasChanges {
try moc.save()
}
}
}catch {
err = error
}
DispatchQueue.main.async {
completion(err)
}
}
}
private func sync(from remoteUsers: [RemoteUser], isFullFilled: Bool) throws {
guard !remoteUsers.isEmpty else { fatalError() }
let ids = remoteUsers.map { $0.userId }
// batch delete
var predicates = [NSPredicate]()
if !isFullFilled {
predicates.append(NSPredicate(format: "%K > %d", #keyPath(User.userId), ids.last!))
}
if remoteUsers.count > 1 {
let (min, max) = (ids.first!, ids.last!)
let key = #keyPath(User.userId)
predicates.append(NSPredicate(format: "%K > %d AND %K < %d AND NOT %K IN %@", key, min, key, max, key, ids))
}
if !predicates.isEmpty {
try batchDeleteUsers { fetchRequest in
fetchRequest.predicate = NSCompoundPredicate(orPredicateWithSubpredicates: predicates)
}
}
// find-or-create
let fetchRequest = User.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(User.userId), ascending: true)]
fetchRequest.predicate = NSPredicate(format: "%K IN %@", #keyPath(User.userId), ids)
let existedUsers = try backgroundContext.fetch(fetchRequest) as! [User]
let idUserMapping = existedUsers.reduce(into: [:]) { $0[$1.userId] = $1 }
remoteUsers.forEach { item in
if let user = idUserMapping[item.userId] {
// update
user.importFrom(item)
}else {
// create
let user = NSEntityDescription.insertNewObject(forEntityName: "User", into: backgroundContext) as! User
user.importFrom(item)
}
}
}
private func batchDeleteUsers(decoration: (NSFetchRequest<NSFetchRequestResult>) -> ()) throws {
let fetchRequest = User.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(User.userId), ascending: true)]
decoration(fetchRequest)
let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
batchDeleteRequest.resultType = .resultTypeObjectIDs
let result = try backgroundContext.execute(batchDeleteRequest) as? NSBatchDeleteResult
if let ids = result?.result as? [NSManagedObjectID], !ids.isEmpty {
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: [NSDeletedObjectsKey: ids], into: [container.viewContext])
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment