Skip to content

Commit

Permalink
helper methods (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
alemar11 authored Nov 20, 2020
1 parent b569563 commit 7aa3e8e
Show file tree
Hide file tree
Showing 79 changed files with 630 additions and 176 deletions.
2 changes: 1 addition & 1 deletion CoreDataPlus.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -395,8 +395,8 @@
isa = PBXGroup;
children = (
236526AA215A46C200A51C9F /* CoreDataPlusInMemoryTestCase.swift */,
23FFB86122EC948C00391D40 /* Utils.swift */,
239F4CBF22F49F2F007888BA /* CoreDataPlusOnDiskTestCase.swift */,
23FFB86122EC948C00391D40 /* Utils.swift */,
);
path = Utils;
sourceTree = "<group>";
Expand Down
21 changes: 20 additions & 1 deletion Sources/NSFetchRequestResult+CoreData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ extension NSFetchRequestResult where Self: NSManagedObject {
return name
}
// Attention: sometimes entity() returns nil due to a CoreData bug occurring in the Unit Test targets or when Generics are used.
// The bug seems fixed on Xcode 12
// The bug seems fixed on Xcode 12 but having a fallback option never hurts.
// https://forums.developer.apple.com/message/203409#203409
// https://stackoverflow.com/questions/37909392/exc-bad-access-when-calling-new-entity-method-in-ios-10-macos-sierra-core-da
// https://stackoverflow.com/questions/43231873/nspersistentcontainer-unittests-with-ios10/43286175
Expand All @@ -31,6 +31,25 @@ extension NSFetchRequestResult where Self: NSManagedObject {
return fetchRequest
}

/// **CoreDataPlus**
///
/// - Returns: an object for a specified `id` even if the object needs to be fetched.
/// If the object is not registered in the context, it may be fetched or returned as a fault.
/// If use existingObject(with:) if you don't want a faulted object.
public static func object(with id: NSManagedObjectID, in context: NSManagedObjectContext) -> Self? {
return context.object(with: id) as? Self
}

/// **CoreDataPlus**
///
/// - Returns: the object for the specified ID or nil if the object does not exist.
/// If there is a managed object with the given ID already registered in the context, that object is returned directly; otherwise the corresponding object is faulted into the context.
/// This method might perform I/O if the data is uncached.
/// - Important: Unlike object(with:), this method never returns a fault.
public static func existingObject(with id: NSManagedObjectID, in context: NSManagedObjectContext) throws -> Self? {
return try context.existingObject(with: id) as? Self
}

/// **CoreDataPlus**
///
/// Performs a configurable fetch request in a context.
Expand Down
12 changes: 12 additions & 0 deletions Sources/NSManagedObjectContext+Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ extension NSManagedObjectContext {
// MARK: - Save

extension NSManagedObjectContext {
/// **CoreDataPlus**
///
/// Checks whether there are actually changes that will change the persistent store.
/// - Note: The `hasChanges` method would return `true` for transient changes as well which can lead to false positives.
public var hasPersistentChanges: Bool {
guard hasChanges else { return false }
return !insertedObjects.isEmpty || !deletedObjects.isEmpty || updatedObjects.first(where: { $0.hasPersistentChangedValues }) != nil
}

/// **CoreDataPlus**
///
/// Asynchronously performs changes and then saves them.
Expand All @@ -91,6 +100,9 @@ extension NSManagedObjectContext {
var internalError: NSError?
do {
try changes(self)
// TODO
// add an option flag to decide whether or not a context can be saved if only transient properties are changed
// in that case hasPersistentChanges should be used instead of hasChanges
if self.hasChanges {
try self.save()
}
Expand Down
147 changes: 139 additions & 8 deletions Tests/NSManagedObjectContextInvestigationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,13 @@ final class NSManagedObjectContextInvestigationTests: CoreDataPlusInMemoryTestCa
childContext.automaticallyMergesChangesFromParent = true

let childCar = try childContext.performAndWaitResult { context -> Car in
let car = try childContext.existingObject(with: car1.objectID) as! Car
let car = try XCTUnwrap(try Car.existingObject(with: car1.objectID, in: context))
XCTAssertEqual(car.maker, "FIAT")
return car
}

try parentContext.performSaveAndWait { context in
let car = try context.existingObject(with: car1.objectID) as! Car
let car = try XCTUnwrap(try Car.existingObject(with: car1.objectID, in: context))
XCTAssertEqual(car.maker, "FIAT")
car.maker = "😀"
XCTAssertEqual(car.maker, "😀")
Expand Down Expand Up @@ -102,13 +102,13 @@ final class NSManagedObjectContextInvestigationTests: CoreDataPlusInMemoryTestCa
childContext.automaticallyMergesChangesFromParent = false

let childCar = try childContext.performAndWaitResult { context -> Car in
let car = try childContext.existingObject(with: car1.objectID) as! Car
let car = try XCTUnwrap(try Car.existingObject(with: car1.objectID, in: context))
XCTAssertEqual(car.maker, "FIAT")
return car
}

try parentContext.performSaveAndWait { context in
let car = try context.existingObject(with: car1.objectID) as! Car
let car = try XCTUnwrap(try Car.existingObject(with: car1.objectID, in: context))
XCTAssertEqual(car.maker, "FIAT")
car.maker = "😀"
XCTAssertEqual(car.maker, "😀")
Expand All @@ -132,13 +132,13 @@ final class NSManagedObjectContextInvestigationTests: CoreDataPlusInMemoryTestCa
childContext.automaticallyMergesChangesFromParent = true

let childCar = try childContext.performAndWaitResult { context -> Car in
let car = try childContext.existingObject(with: car1.objectID) as! Car
let car = try XCTUnwrap(try Car.existingObject(with: car1.objectID, in: context))
XCTAssertEqual(car.maker, "FIAT")
return car
}

try parentContext.performSaveAndWait { context in
let car = try context.existingObject(with: car1.objectID) as! Car
let car = try XCTUnwrap(try Car.existingObject(with: car1.objectID, in: context))
XCTAssertEqual(car.maker, "FIAT")
car.maker = "😀"
XCTAssertEqual(car.maker, "😀")
Expand All @@ -163,13 +163,13 @@ final class NSManagedObjectContextInvestigationTests: CoreDataPlusInMemoryTestCa
childContext.automaticallyMergesChangesFromParent = false

let childCar = try childContext.performAndWaitResult { context -> Car in
let car = try childContext.existingObject(with: car1.objectID) as! Car
let car = try XCTUnwrap(try Car.existingObject(with: car1.objectID, in: context))
XCTAssertEqual(car.maker, "FIAT")
return car
}

try parentContext.performSaveAndWait { context in
let car = try context.existingObject(with: car1.objectID) as! Car
let car = try XCTUnwrap(try Car.existingObject(with: car1.objectID, in: context))
XCTAssertEqual(car.maker, "FIAT")
car.maker = "😀"
XCTAssertEqual(car.maker, "😀")
Expand Down Expand Up @@ -253,4 +253,135 @@ final class NSManagedObjectContextInvestigationTests: CoreDataPlusInMemoryTestCa
XCTAssertEqual(readEntity?.maker, "FCA")
}
}

func testInvestigationTransientProperties() throws {
let container = InMemoryPersistentContainer.makeNew()
let viewContext = container.viewContext

let car = Car(context: viewContext)
car.maker = "FIAT"
car.model = "Panda"
car.numberPlate = UUID().uuidString
car.currentDrivingSpeed = 50
try viewContext.save()

XCTAssertEqual(car.currentDrivingSpeed, 50)
viewContext.refreshAllObjects()
XCTAssertEqual(car.currentDrivingSpeed, 0)
car.currentDrivingSpeed = 100
XCTAssertEqual(car.currentDrivingSpeed, 100)
viewContext.reset()
XCTAssertEqual(car.currentDrivingSpeed, 0)
}

func testXXX() throws {
let container = InMemoryPersistentContainer.makeNew()
let viewContext = container.viewContext

viewContext.performAndWait {
let car = Car(context: viewContext)
car.maker = "FIAT"
car.model = "Panda"
car.numberPlate = UUID().uuidString
car.currentDrivingSpeed = 50
try! viewContext.save()
}

viewContext.performAndWait {
print(viewContext.registeredObjects)
}
}

func testInvestigationTransientPropertiesBehaviorInParentChildContextRelationship() throws {
let container = InMemoryPersistentContainer.makeNew()
let viewContext = container.viewContext
let childContext = viewContext.newBackgroundContext(asChildContext: true)
var carID: NSManagedObjectID?

let plateNumber = UUID().uuidString
let predicate = NSPredicate(format: "%K == %@", #keyPath(Car.numberPlate), plateNumber)

childContext.performAndWait {
let car = Car(context: $0)
car.maker = "FIAT"
car.model = "Panda"
car.numberPlate = plateNumber
car.currentDrivingSpeed = 50
try! $0.save()
carID = car.objectID
XCTAssertEqual(car.currentDrivingSpeed, 50)
print($0.registeredObjects)
car.currentDrivingSpeed = 20 // ⚠️ dirting the context again
}

childContext.performAndWait {
print(childContext.registeredObjects)
}

let id = try XCTUnwrap(carID)
let car = try XCTUnwrap(Car.object(with: id, in: viewContext))
XCTAssertEqual(car.maker, "FIAT")
XCTAssertEqual(car.model, "Panda")
XCTAssertEqual(car.numberPlate, plateNumber)
XCTAssertEqual(car.currentDrivingSpeed, 50, "The transient property value should be equal to the one saved by the child context.")

try childContext.performAndWait {
XCTAssertFalse(childContext.registeredObjects.isEmpty) // ⚠️ this condition is verified only because we have dirted the context after a save
let car = try XCTUnwrap($0.object(with: id) as? Car)
XCTAssertEqual(car.currentDrivingSpeed, 20)
try $0.save()
}

XCTAssertEqual(car.currentDrivingSpeed, 20, "The transient property value should be equal to the one saved by the child context.")

try childContext.performAndWait {
XCTAssertTrue(childContext.registeredObjects.isEmpty) // ⚠️ it seems that after a save, the objects are freed unless the context gets dirted again
let car = try XCTUnwrap(try Car.fetchUnique(in: $0, where: predicate))
XCTAssertEqual(car.currentDrivingSpeed, 0)
}

// see testInvestigationContextRegisteredObjectBehaviorAfterSaving
}

func testInvestigationContextRegisteredObjectBehaviorAfterSaving() throws {
let context = container.newBackgroundContext()

// A context keeps registered objects until it's dirted
try context.performAndWait {
let person = Person(context: context)
person.firstName = "Alessandro"
person.lastName = "Marzoli"
try $0.save()

let person2 = Person(context: context)
person2.firstName = "Andrea"
person2.lastName = "Marzoli"
// context dirted because person2 isn't yet saved
}

context.performAndWait {
XCTAssertFalse(context.registeredObjects.isEmpty)
}

try context.performAndWait {
try $0.save()
// context is no more dirted, everything has been saved
}

context.performAndWait {
XCTAssertTrue(context.registeredObjects.isEmpty)
}

try context.performAndWait {
let person = Person(context: context)
person.firstName = "Valedmaro"
person.lastName = "Marzoli"
try $0.save()
// context is no more dirted, everything has been saved
}

context.performAndWait {
XCTAssertTrue(context.registeredObjects.isEmpty)
}
}
}
78 changes: 73 additions & 5 deletions Tests/NSManagedObjectContextUtilsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,74 @@ final class NSManagedObjectContextUtilsTests: CoreDataPlusInMemoryTestCase {
XCTAssertTrue(context.persistentStores.isEmpty)
}

func testHasPersistentChanges() throws {
let viewContext = container.viewContext
XCTAssertFalse(viewContext.hasPersistentChanges)
let car = Car(context: viewContext)
car.maker = "FIAT"
car.model = "Panda"
car.numberPlate = UUID().uuidString
car.currentDrivingSpeed = 50

XCTAssertTrue(viewContext.hasPersistentChanges)
try viewContext.save()
XCTAssertFalse(viewContext.hasPersistentChanges)
XCTAssertEqual(car.currentDrivingSpeed, 50)
car.currentDrivingSpeed = 10
XCTAssertTrue(car.hasChanges)
XCTAssertFalse(car.hasPersistentChangedValues)
XCTAssertFalse(viewContext.hasPersistentChanges, "viewContext shouldn't have committable changes because only transients properties are changed.")
}

func testHasPersistentChangesInParentChildContextRelationship() throws {
let viewContext = container.viewContext
let backgroundContext = viewContext.newBackgroundContext(asChildContext: true)

backgroundContext.performAndWait {
XCTAssertFalse(backgroundContext.hasPersistentChanges)
let car = Car(context: backgroundContext)
car.maker = "FIAT"
car.model = "Panda"
car.numberPlate = UUID().uuidString
XCTAssertTrue(backgroundContext.hasPersistentChanges)

try! backgroundContext.save()

XCTAssertFalse(backgroundContext.hasPersistentChanges)
XCTAssertEqual(car.currentDrivingSpeed, 0)
car.currentDrivingSpeed = 50
XCTAssertTrue(car.hasChanges)
XCTAssertFalse(car.hasPersistentChangedValues)
XCTAssertFalse(backgroundContext.hasPersistentChanges)

try! backgroundContext.save() // pushing the transient value up to the parent context
}

let car = try XCTUnwrap(try Car.fetchOne(in: viewContext, where: NSPredicate(value: true)))
XCTAssertEqual(car.currentDrivingSpeed, 50)
XCTAssertTrue(viewContext.hasChanges, "The viewContext should have uncommitted changes after the child save.")
XCTAssertTrue(viewContext.hasPersistentChanges, "The viewContext should have uncommitted changes after the child save.")

try viewContext.save()

backgroundContext.performAndWait {
do {
let car = try XCTUnwrap(try Car.fetchOne(in: backgroundContext, where: NSPredicate(value: true)))
XCTAssertEqual(car.currentDrivingSpeed, 0)
car.currentDrivingSpeed = 30
XCTAssertTrue(backgroundContext.hasChanges)
XCTAssertFalse(backgroundContext.hasPersistentChanges, "backgroundContext shouldn't have committable changes because only transients properties are changed.")
try backgroundContext.save()
} catch let error as NSError {
XCTFail(error.description)
}
}

XCTAssertTrue(viewContext.hasChanges, "The transient property has changed")
XCTAssertEqual(car.currentDrivingSpeed, 30)
XCTAssertFalse(viewContext.hasPersistentChanges, "viewContext shouldn't have committable changes because only transients properties are changed.")
}

func testMetaData() {
// When
guard let firstPersistentStore = container.viewContext.persistentStores.first else {
Expand Down Expand Up @@ -487,18 +555,18 @@ final class NSManagedObjectContextUtilsTests: CoreDataPlusInMemoryTestCase {

func testPerformSaveUpToTheLastParentContextAndWait() throws {
let mainContext = container.viewContext
let backgroundContext = mainContext.newBackgroundContext(asChildContext: true) // main context children
let childBackgroundContext = backgroundContext.newBackgroundContext(asChildContext: true) // background context children
let backgroundContext = mainContext.newBackgroundContext(asChildContext: true) // main context child
let childBackgroundContext = backgroundContext.newBackgroundContext(asChildContext: true) // background context child

childBackgroundContext.performAndWaitResult { context in
childBackgroundContext.performAndWait { context in
let person = Person(context: context)
person.firstName = "Alessandro"
person.lastName = "Marzoli"
}

try childBackgroundContext.performSaveUpToTheLastParentContextAndWait()
try backgroundContext.performAndWaitResult { _ in
let count = try Person.count(in: backgroundContext)
try backgroundContext.performAndWait {
let count = try Person.count(in: $0)
XCTAssertEqual(count, 1)
}

Expand Down
2 changes: 1 addition & 1 deletion Tests/Notifications/NotificationMergeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ final class NotificationMergeTests: CoreDataPlusInMemoryTestCase {
person.firstName = "Edythe"
person.lastName = "Moreton"

let person2 = backgroundContext.object(with: person2.objectID) as! Person
let person2 = try XCTUnwrap(Person.object(with: person2.objectID, in: backgroundContext))
person2.firstName += "**"
}

Expand Down
1 change: 1 addition & 0 deletions Tests/Resources/SampleModel/Entities/Car.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class Car: NSManagedObject {
@NSManaged public var model: String?
@NSManaged public var numberPlate: String!
@NSManaged public var owner: Person?
@NSManaged public var currentDrivingSpeed: Int // transient property
}

extension Car: DelayedDeletable {
Expand Down
Loading

0 comments on commit 7aa3e8e

Please sign in to comment.