Created
September 2, 2024 09:32
-
-
Save arashkashi/3d963b53a1aa5caf789f8b3a7838579c to your computer and use it in GitHub Desktop.
Duplicate Core Data items iOS Swift with all attributes and relationships
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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