Skip to content

Instantly share code, notes, and snippets.

@arashkashi
Created September 2, 2024 09:32
Show Gist options
  • Save arashkashi/3d963b53a1aa5caf789f8b3a7838579c to your computer and use it in GitHub Desktop.
Save arashkashi/3d963b53a1aa5caf789f8b3a7838579c to your computer and use it in GitHub Desktop.
Duplicate Core Data items iOS Swift with all attributes and relationships
import Foundation
import Combine
import Logging
// swiftlint:disable empty_count
protocol DuplicatableCoreDataEntity {
var asyncID: String { get async throws}
func setDuplicatedID() async throws -> String
}
extension Bike: DuplicatableCoreDataEntity {
func setDuplicatedID() async throws -> String {
try await withCheckedThrowingContinuation { continuation in
guard let moc = self.managedObjectContext else {
continuation.resume(throwing: ApiError.apiError(reason: "No managed object context"))
return
}
Task {
do {
let newAsyncId = (try await self.asyncID) + "_duplicated_\(Date.now)"
await moc.perform {
self.vin = newAsyncId
continuation.resume(returning: newAsyncId)
}
} catch {
continuation.resume(throwing: error)
}
}
}
}
var asyncID: String {
get async throws {
try await withCheckedThrowingContinuation { continuation in
guard let moc = self.managedObjectContext else {
let error = ApiError.apiError(reason: "Error in:" + "\(logFileName())" + "Could not get the managed object context")
continuation.resume(throwing: error)
return
}
moc.perform {
if let vin = self.vin {
continuation.resume(returning: vin)
} else {
continuation.resume(throwing: ApiError.apiError(reason: "Error in:"+"\(logFileName())" + "Does not seem to have a valid id"))
}
}
}
}
}
}
extension NSManagedObject {
static var bag = Set<AnyCancellable>()
func delete() async -> [BikeSyncTestEvent] {
await withCheckedContinuation { cont in
self.delete { events in
cont.resume(returning: events)
}
}
}
func delete(completion: @escaping ([BikeSyncTestEvent]) -> Void) {
guard let moc = self.managedObjectContext else {
return completion( [.fail(ApiError.apiError(reason: "Error in:" + "\(logFileName())" + "failed to delete!"))])
}
moc.perform {
moc.delete(self)
do {
try self.managedObjectContext?.save()
completion([.info("Deleted object from core data")])
} catch {
return completion( [.fail(ApiError.apiError(reason: "could not delete from core data \(error.localizedDescription)"))])
}
}
}
func duplicateWithJustAttributes() async -> ([BikeSyncTestEvent], NSManagedObject?) {
await withCheckedContinuation { cont in
duplicateWithJustAttributes { events, object in
cont.resume(returning: (events, object))
}
}
}
fileprivate func duplicateWithJustAttributes(completion: @escaping ([BikeSyncTestEvent], NSManagedObject?) -> Void) {
guard let name = self.entity.name, let moc = self.managedObjectContext else {
completion([.fail(ApiError.apiError(reason: "\(logFileName())"))], nil)
return
}
moc.name = "insertion"
moc.perform {
guard let finalDuplicatedResult = NSEntityDescription.insertNewObject(forEntityName: name, into: moc) as? Bike else {
completion([.fail(ApiError.apiError(reason: "\(logFileName())"))], nil)
return
}
assert("insertion" == finalDuplicatedResult.managedObjectContext?.name)
for attribute in self.entity.attributesByName {
finalDuplicatedResult.setValue(self.value(forKey: attribute.key), forKey: attribute.key)
}
completion([.info("\(#function) succeeded!")], finalDuplicatedResult)
}
}
func duplicateAndAssignOneToOneRelationships(to destinationObject: NSManagedObject) async -> ([BikeSyncTestEvent], [NSManagedObject]) {
await withCheckedContinuation { cont in
duplicateAndAssignOneToOneRelationships(to: destinationObject) { events, objects in
cont.resume(returning: (events, objects))
}
}
}
func duplicateAndAssignOneToOneRelationships(to destinationObject: NSManagedObject, completion: @escaping ([BikeSyncTestEvent], [NSManagedObject]) -> Void ) {
guard let moc = self.managedObjectContext else {
completion([.fail(ApiError.apiError(reason: "\(logFileName())"))], [])
return
}
moc.perform { [weak self] in
self?
.entity
.relationshipsByName
.publisher
.flatMap(maxPublishers: .max(1)) { relationship -> AnyPublisher<([BikeSyncTestEvent], NSManagedObject?), Never> in
Future<([BikeSyncTestEvent], NSManagedObject?), Never> { promise in
moc.perform {
let value = self?.value(forKey: relationship.key)
if let singleObjectRelation = value as? NSManagedObject {
if singleObjectRelation.isFault {
moc.refresh(singleObjectRelation, mergeChanges: true)
}
singleObjectRelation.duplicate { events, duplicatedOptional in
if let duplicatedOptional {
destinationObject.setValue(duplicatedOptional, forKey: relationship.key)
promise(.success((events + [.info("One to one Relationship is duplicated successfully! - \(relationship.key)")], duplicatedOptional)))
} else {
promise(.success((events, nil)))
}
}
} else {
promise(.success(([.info("no one to one relationship \(relationship.key)")], nil)))
}
}
}
.eraseToAnyPublisher()
}
.collect()
.eraseToAnyPublisher()
.sink(receiveValue: { listResult in
var events: [BikeSyncTestEvent] = []
var duplicatedObjects: [NSManagedObject] = []
for item in listResult {
events += item.0
if let valid = item.1 {
duplicatedObjects.append(valid)
}
}
completion(events, duplicatedObjects)
})
.store(in: &Self.bag)
}
}
func duplicateAndAssignOneToManyRelationships(to destinationObject: NSManagedObject) async -> ([BikeSyncTestEvent], [String: [NSManagedObject]]) {
await withCheckedContinuation { cont in
duplicateAndAssignOneToManyRelationships(to: destinationObject) { events, relationships in
cont.resume(returning: (events, relationships))
}
}
}
func duplicateAndAssignOneToManyRelationships(to destinationObject: NSManagedObject, completion: @escaping ([BikeSyncTestEvent], [String: [NSManagedObject]]) -> Void ) {
guard let moc = self.managedObjectContext else {
completion([.fail(ApiError.apiError(reason: "\(logFileName())"))], [:])
return
}
moc.perform {
self
.entity
.relationshipsByName
.publisher
.flatMap(maxPublishers: .max(1)) { relationship -> AnyPublisher<([BikeSyncTestEvent], [String: [NSManagedObject]]), Never> in
Future<([BikeSyncTestEvent], [String: [NSManagedObject]]), Never> { promise in
let group = DispatchGroup()
var mutableSet = NSMutableSet()
var resultEvents: [BikeSyncTestEvent] = []
defer {
group.notify(queue: .main) {
moc.perform {
let info = resultEvents + [BikeSyncTestEvent.info("One to many relationships duplicated successfully!")]
let objects: [NSManagedObject] = Array(_immutableCocoaArray: mutableSet)
promise(.success((info, [relationship.key: objects])))
}
}
}
if let valueSet = self.value(forKey: relationship.key) as? NSSet, valueSet.count > 0 {
let allObjectsInRelationshipSet = valueSet.allObjects.map({ object in
if let object = object as? NSManagedObject, object.isFault == true {
moc.refresh(object, mergeChanges: true)
return object
} else {
return object as? NSManagedObject
}
}).compactMap { $0 }
for object in allObjectsInRelationshipSet {
group.enter()
object.duplicate { events, duplicatedOptional in
if let duplicatedOptional {
mutableSet.add(duplicatedOptional)
} else {
resultEvents.append(.fail(ApiError.apiError(reason: "\(logFileName())")))
}
group.leave()
}
}
} else {
promise(.success(([.info("No one to many relationship! \(relationship.key)")], [:])))
}
}
.eraseToAnyPublisher()
}
.collect()
.eraseToAnyPublisher()
.sink(receiveValue: { listResult in
moc.perform {
var events: [BikeSyncTestEvent] = []
var duplicatedObjectsMap: [String: [NSManagedObject]] = [:]
for item in listResult {
events += item.0
for (key, value) in item.1 {
duplicatedObjectsMap[key] = value
self.setValue(Set(value), forKey: key)
}
}
completion(events, duplicatedObjectsMap)
}
})
.store(in: &Self.bag)
}
}
func duplicate() async -> ([BikeSyncTestEvent], NSManagedObject?) {
await withCheckedContinuation { cont in
duplicate { events, object in
cont.resume(returning: (events, object))
}
}
}
func duplicate(completion: @escaping ([BikeSyncTestEvent], NSManagedObject?) -> Void) {
Task {
var isTaskCancelled = false
var duplicationEvents: [BikeSyncTestEvent] = []
func onEventsUpdate(_ events: [BikeSyncTestEvent]) {
let hasError = events
.contains { event in
if case .fail = event {
return true
} else {
return false
}
}
if hasError { isTaskCancelled = true }
}
// 1. Duplicate just the immediate attributes.
let firstStageResult = await self.duplicateWithJustAttributes()
duplicationEvents += firstStageResult.0
LogInfo("++++++******\(logFileName())")
onEventsUpdate(duplicationEvents)
guard isTaskCancelled == false, firstStageResult.1 != nil else {
duplicationEvents += await firstStageResult.1?.delete() ?? []
LogInfo("++++++******\(logFileName())")
completion(duplicationEvents, nil)
return
}
// 2. Duplicate the one to one relationships.
let directAttributeAssignmentsResult = await self.duplicateAndAssignOneToOneRelationships(to: firstStageResult.1!)
duplicationEvents += directAttributeAssignmentsResult.0
onEventsUpdate(duplicationEvents)
LogInfo("++++++******\(logFileName())")
guard isTaskCancelled == false else {
duplicationEvents += await firstStageResult.1?.delete() ?? []
LogInfo("++++++******\(logFileName())")
completion(duplicationEvents, nil)
return
}
// 3. Duplicate the one to many relationships.
let oneToManyResult = await self.duplicateAndAssignOneToManyRelationships(to: firstStageResult.1!)
duplicationEvents += oneToManyResult.0
LogInfo("++++++******\(logFileName())")
onEventsUpdate(duplicationEvents)
guard isTaskCancelled == false else {
duplicationEvents += await firstStageResult.1?.delete() ?? []
LogInfo("++++++******\(logFileName())")
completion(duplicationEvents, nil)
return
}
LogInfo("++++++******\(logFileName())")
completion(duplicationEvents, firstStageResult.1)
}
}
}
func logFileName(file: String = #file, line: Int = #line) -> String {
let fileName = URL(fileURLWithPath: file).lastPathComponent
return "\(fileName):\(line)"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment