How to set merge policy in Swift 4 CoreData - ios

I'm attempting to update an entity in CoreData. Here are some of my declarations.
static var appDelegate = UIApplication.shared.delegate as! AppDelegate
static var context = appDelegate.persistentContainer.viewContext
static var entity = NSEntityDescription.entity(forEntityName: "PData", in: context)
static var newPData = NSManagedObject(entity: entity!, insertInto: context)
I'm somewhat certain the fact that they're static isn't relevant.
PData is the name of the entity (short for persistent data).
Later on, I set the values I'd like to save using newPData.setValue("foo", forKey: "bar"), but when I actually attempt to save them with context.save(), I get NSCocoaErrorDomain Code 133020 "Could not merge changes."
I should mention that this is meant to occur directly after deleting an existing entry in PData (replacing an old entity instance with a new one).
I've done some reading, and I've found that the reason for this error is that the default way that Swift handles a CoreData merge conflict is to throw an error. I'd like to change my CoreData settings such that changes from memory overwrite changes already stored within the entity in CoreData, but I'm not sure as to how I'd going about doing this.
The Apple documentation shows lots of different merge policy options, but doesn't have an example showing how to implement them. I think the one I need to use is NSMergeByPropertyStoreTrumpMergePolicy, but I have no idea how to actually set said policy as the merge policy.

I found the answer - to set the context's merge policy, you simply do
context.mergePolicy = NSMergePolicy(merge: NSMergePolicyType.mergeByPropertyObjectTrumpMergePolicyType)
I was getting tripped up by trying to do
context.mergePolicy = mergeByPropertyObjectTrumpMergePolicyType)
but I guess it's necessary to spawn an NSMergePolicy object. I just assumed that the actual merge policy (mergeByPropertyObjectTrumpMergePolicyType) would be of the correct type by default.

You can put mergePolicy when initiate persistentContainer
var persistentContainer: NSPersistentContainer = {
let modelURL = Bundle.main.url(forResource: DB_NAME, withExtension: "momd")!
let container = NSPersistentContainer.init(name: DB_NAME, managedObjectModel: NSManagedObjectModel(contentsOf: modelURL)!)
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
if let error = error as NSError? {
QiscusLogger.errorPrint("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()

Merge policies are used in coredata to resolve the conflict issue between persistent store and different managed object context, it depends on you which merge policy is suitable for your application.
As from the code snippet it seams that you are using single managed object conext.
Please try below code-
appDelegate.persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
appDelegate.persistentContainer.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyStoreTrump

In Swift 5 once you create a project with Core Data, then you have Persistence.swift file in your project directory.
In this class put mergePolicy code in the last of init() function.
init(inMemory: Bool = false) {
....
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
}
And it's work for me 😉.

Related

Create CoreData object in tests with a simple init

I have a set of NSManagedObject subclasses which are used by the ClassToBeTested.
The ClassToBeTested operates just on a few properties of the NSManagedObject subclasses and doesn't need relationships or the whole CoreData stack.
Can I somehow use the same objects in tests by just creating them in a regular way:
let template = CoreDataClass()
template.name = randomString(length: 40) // This fails!
templates.append(template)
Currently it fails with error:
failed: caught "NSInvalidArgumentException", "-[CoreDataClass
setTemplate_name:]: unrecognized selector sent to instance
0x600000af4c40"
Although I get a different error (did not call designated initializer) when I try to do that, in either case, the answer to your question is: No, you cannot do that.
But with NSPersistentContainer nowadays, it is easy to use a singleton in-memory Core Data Stack for such testing. Include your data model in your test bundle, then put this in your test's global scope:
var sharedTestContext: NSManagedObjectContext = {
// Swift is smart enough to execute this only once.
let container = NSPersistentContainer(name: "<YourDataModelName>")
let description = NSPersistentStoreDescription()
description.type = NSInMemoryStoreType
container.persistentStoreDescriptions = [description]
container.loadPersistentStores { (description, error) in
if let error = error {
fatalError("Failed to load store for test: \(error)")
}
}
return container.newBackgroundContext()
}()
And define a special managed object initializer for testing like this:
/**
Initializes a managed object for testing
- important: This assumes your managed object subclass name is the same
as its entity name.
*/
public extension NSManagedObject {
convenience init(testContext: NSManagedObjectContext?) {
let context = testContext ?? sharedTestContext
/* The following contraption is necessary to avoid warning:
"Multiple NSEntityDescriptions claim the NSManagedObject subclass"
For explanation see:
https://stackoverflow.com/questions/51851485/multiple-nsentitydescriptions-claim-nsmanagedobject-subclass */
let name = String(describing: type(of: self))
let entity = NSEntityDescription.entity(forEntityName: name, in: context)!
self.init(entity: entity, insertInto: context)
}
}
Now you can create your test object thus:
let template = CoreDataClass(testContext: nil)

Swift 4 Unit Testing with Core Data

I have an iOS project that is working fine on the simulator and whatnot, but cannot seem to link up nicely to the Core Data resources when I'm working from its testing bundle.
I've made the NSManagedObjectContext from memory in the set-up function of my testing class. However, when I attempt to run the program, the test functions fail, and the console has output
"An NSManagedObject of class 'Projectname.Deck' must have a valid NSEntityDescription."
Is there something I'm missing? I'd like to be able to make unit tests for my app's data structure as I develop it.
Thanks!
Edit
Relevant sections of the test class:
class ProjectNameTests: XCTestCase {
var testDeck: Deck? = nil
func setUpInMemoryManagedObjectContext() -> NSManagedObjectContext {
let managedObjectModel = NSManagedObjectModel.mergedModel(from: [Bundle.main])!
let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel)
do {
try persistentStoreCoordinator.addPersistentStore(ofType: NSInMemoryStoreType, configurationName: nil, at: nil, options: nil)
} catch {
print("Adding in-memory persistent store failed")
}
let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator
return managedObjectContext
}//setUpInMemoryManagedObjectContext
override func setUp() {
super.setUp()
self.context = setUpInMemoryManagedObjectContext()
testDeck = Deck(context: context)
testDeck!.name = "Test Deck"
}//setUp
}//ProjectNameTests
UPDATE:
After doing a fair bit of research, I was able to figure out that my issue was related to pulling the NSEntityDescription from the class itself, and not from the current context.
I added the following method to my Deck class (and will do the same for other NSManagedObject subclasses):
public static func entityDescription(context: NSManagedObjectContext)->NSEntityDescription{
return NSEntityDescription.entity(forEntityName: String(describing: self), in: context)!
}//entityDescription
I then changed the initialization call in my test case to the following:
let deckEntity: NSEntityDescription = Deck.entityDescription(context: context)
testDeck = Deck(entity: deckEntity, insertInto: context)
This way, the object is initialized with the NSEntityDescription pulled from the current NSManagedObjectContext, which makes everything happier.
Props to Swift, Core Data, and unit testing for leading me on the right track.

Programmatically create entity/table using CoreData and insert values into that entity in Swift 3 for iOS

I have been trying to find a way to programmatically create the database entity/table at runtime in Swift 3 for iOS platform.
Please explain the process in detail. I have followed other links but nothing worked for me and I was unable to understand the process properly so please explain the whole process with working code instead of providing other existing links.
There is one more table which has been added from XCode and I can perform the database related operations (insert row, delete, update) on that table but I am unable to find a way to perform following operations on the table which will be created programmatically at run time.
There are following operations that need to be performed.
Add Entity - From interface, user can add entity name, number of columns, column names then on tapping a button the entity should be created into the database using CoreData. User can add any number of tables from the interface.
Insert Values - Then user will have interface to insert values into the entity. The name of the entity will be known and the values for a row in the entity will be entered by user. On tapping a button the row should be inserted into the entity/database-table.
Fetch Values - User will have an option to fetch all the saved values form the entity. To use them anytime when needed.
Update value - User will be able to update any value present in the table which was created programmatically.
Delete Entity - There will be an option to delete the database entity as well. On tapping a button, the name of the entity will be passed to the function and that entity/database-table will be deleted from the database.
I have tried the following code -:
let model = NSManagedObjectModel()
let entityName = "TestEntity"
let entity = NSEntityDescription()
entity.name = entityName
entity.managedObjectClassName = entityName
// Creating a attribute
let remoteURLAttribute = NSAttributeDescription()
remoteURLAttribute.name = "columnOne"
remoteURLAttribute.attributeType = .stringAttributeType
remoteURLAttribute.isOptional = true
remoteURLAttribute.isIndexed = true
var properties = Array<NSAttributeDescription>()
properties.append(remoteURLAttribute)
// Adding the attribute to the entity
entity.properties = properties
// Adding the entity into the model
model.entities = [entity]
do {
try CoreDataManager.managedObjectContext.save()
} catch {
print(error)
}
// Inserting the data into the entity
let testEntity = NSManagedObject(entity: entity, insertInto: CoreDataManager.managedObjectContext)
testEntity.setValue("WHY SO SERIOUS?", forKey: "columnOne")
do {
try CoreDataManager.managedObjectContext.save()
} catch {
print(error)
}
// Fetching the data from the entity
let request = NSFetchRequest<NSFetchRequestResult>()
request.entity = entity
request.propertiesToFetch = ["columnOne"]
request.returnsDistinctResults = false
do {
let results = try CoreDataManager.managedObjectContext.fetch(request)
print(results)
} catch {
}
I am getting the following value in the results -:
â–¿ 1 element
- 0 : (entity: TestEntity; id: 0x600000422f80
;
data: {
columnOne = "WHY SO SERIOUS?"; })
But I don't have any way to get the values from this entity anywhere else and insert the values into the entity from other places as well by using entity name which is TestEntity in this case.
Some of the steps you describe are simply not possible with Core Data. Or at least, not without making things extremely complex and difficult for you. Asking for complete working code is not reasonable because you have an extremely unusual and complex set of requirements for Core Data.
It's true that the NSManagedModel and all of the entities are mutable and can be changed in code. Except, all of these changes must be made before you use that model to load any data. You cannot make changes after you call loadPersistentStores(_:) (if you're using NSPersistentContainer) or addPersistentStore(ofType:configurationName:at:options:) (if you're not using NSPersistentContainer). Doing so is a run-time error that will crash your app.
Because of this, the idea of providing a user interface to dynamically create and delete Core Data entities is not really an option. You can create and delete instances of entities but not the entity descriptions.
If you're willing to do a lot of hard work, you might be able to get something close. The closest you could get with Core Data would be to create an entirely new managed object model any time you need a change. Then copy all of your data to a new persistent store file using this model (including whatever custom code you need to handle non-lightweight migration). Then delete the old model and persistent store file and all references to all objects that you previously fetched with them.
This will be very difficult to get right and may have unforeseen difficulties that I haven't thought of. I would strongly urge not attempting to do this with Core Data, because it was not designed to be used in this way.
It can be done quit simply with the following code:
import Foundation
import CoreData
class CoreData {
static let shared = CoreData()
let dataBase = NSManagedObjectModel()
let dbTable:NSEntityDescription = {
let tableEntity = NSEntityDescription()
tableEntity.name = "NEntity"
tableEntity.managedObjectClassName = "NEntity"
return tableEntity
}()
// The attributes each item in the table will have. e.g. id, name, password, date
let dbTableAttribute:NSAttributeDescription = {
let tblAttr = NSAttributeDescription()
tblAttr.name = "id"
tblAttr.attributeType = .integer64AttributeType
tblAttr.isOptional = true
return tblAttr
}()
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "APICall", managedObjectModel: dataBase)
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
private init(){
dataBase.entities.append(dbTable)
dbTable.properties.append(dbTableAttribute)
}
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}
// MARK: - Defining the class for the table, you can put this in a seperate file if you want but for ease of copying I put them together
#objc(NEntity)
public class NEntity: NSManagedObject {
// Variables must be NSManaged to be saved to the database
#NSManaged var id:Int64
// Call this function to save a new item to the table
static func addEntity(_ id:Int) {
let entityName = NSStringFromClass(self)
let moc = CoreData.shared.persistentContainer.viewContext
guard let entity = NSEntityDescription.insertNewObject(forEntityName:entityName, into: moc)
as? NEntity
else { print("Could not find entity: \(entityName) in database: \(CoreData.shared.persistentContainer.name)"); return }
entity.id = Int64(id)
// Update database
CoreData.shared.saveContext()
}
//Call this function to search the database for items matching the id, see removeEntityWithAttribute() for example
func fetchEntityByAttribute(id:Int, completion:#escaping ([NEntity]?) -> ()){
let moc = CoreData.shared.persistentContainer.viewContext
let fetchRequest = NEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "id == %d", id)
moc.perform {
guard let result = try? fetchRequest.execute() else {
return
}
if result.count > 0 {
completion((result as! [NEntity]))
}else {
completion(nil)
}
}
}
//Call this function to delete entities from the database with the matching id
func removeEntityWithAttribute(id:Int) {
let moc = CoreData.shared.persistentContainer.viewContext
fetchEntityByAttribute(id:id) { matchedEntities in
if let matchingEntities = matchedEntities {
matchingEntities.forEach(){
moc.delete($0)
CoreData.shared.saveContext()
}
}
}
}
}
Just call NEntity.addEntity(5) anywhere in your code to save an item in the database with an id of 5. Be sure to give them unique ids so you can edit and delete them.
Sources:
https://tigi44.github.io/ios/iOS,-Swift-Core-Data-Model-in-a-Swift-Package/
https://dmytro-anokhin.medium.com/core-data-and-swift-package-manager-6ed9ff70921a

Core data temporary database for unit tests

Is this possible to create new temporary core data database when I run tests?
Because I have a problem, when I run my test I create wishlist:
import XCTest
#testable import TestProj
class ChangeWishListTests: XCTestCase {
func testSaveWishList() {
self.wishList = self.changeWishListVC?.saveWishList(title: "Test wish list",
desc: "My description",
wishlistType: WishListType.Shared,
hidden: false)
XCTAssertNotNil(wishList, "Wishlist not created.")
}
}
Than it appears in simulator. Or if it is impossible, how can I manage my fake objects.
Yes, you can but in order to do that you have to be able to change (tell the VC) managed object context that is used to perform Core Data operations. By doing you can then user test managed object in your test and the true one in production application code.
By test managed object context I mean the one that stores data only in memory without saving anything to the disc - the results of operations performed on that kind of context aren't persisted between different launches of the tests.
Creating managed object context that stores data only in memory is pretty simple:
let managedObjectModel = NSManagedObjectModel.init(contentsOfURL: modelURL)
var managedObjectContext = NSManagedObjectContext.init(concurrencyType: .MainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = NSPersistentStoreCoordinator.init(managedObjectModel: managedObjectModel)
var error: NSError?
let options = [NSMigratePersistentStoresAutomaticallyOption : true, NSInferMappingModelAutomaticallyOption : true];
let persistentStore = try! managedObjectContext.persistentStoreCoordinator?.addPersistentStoreWithType(NSInMemoryStoreType, configuration: nil, URL: nil, options: options)
The simplest way to give yourself possibility to use test managed object context is to use dependency injection. Create initializer of your VC that takes managed object context as an argument - inject test managed object context in test code and normal managed object context in production code.

How to access NSManagedObject entity using objectID which is not saved yet in iOS?

I have created one entity using private queue MOC and called performBlock() to save it asynchronously.
Let’s say I want this newly created entity in other context / MOC, so I have only one way to access it is use object id.
But as it’s asynchronous call to save entity, it is possibility of situation where entity is not saved yet and I want to access it immediately after performBlock(), but on other context.
What is the way to access it in this situation?
Thanks in advance!
Update
let privateMOC = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
privateMOC.parentContext = mainThreadManagedObjectContext()
let newUser = Contact(nameStr: "name", moc: privateMOC)
This is my generic code to create new entities like:
init(nameStr: String, moc:NSManagedObjectContext) {
let mEntity = NSEntityDescription.entityForName("Contact", inManagedObjectContext: moc)
super.init(entity: mEntity!, insertIntoManagedObjectContext: moc)
name = nameStr
}
After creating, I am saving it immediately as:
performBlock { () -> Void in
do {
try privateMOC.save()
}catch {
fatalError("Error asynchronously saving private moc - \(errorMsg): \(error)")
}
}
//accessing
let privateMOC = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
privateMOC.parentContext = mainThreadManagedObjectContext()
let pred = NSPredicate(format: "name == %#", "name")
if let results = DatabaseInterface.sharedInstance.fetchEntityFromCollection("Contact", withPredicate: pred, moc: privateMOC) {
if let user = results.last {
return user as! Contact
}
}
else {
DDLogWarn("No user found")
}
I am not getting any user, but I can see this new contact is saved in DB when look using sqlitebrowser
Forgive me for indentation of code :(
What you're asking is impossible.
When you create a managed object on one context, it only exists in memory until you save changes. No other managed object context can get to it, even if you use the object ID. The other context can't look up the data from the persistent store file, and it can't get it from the other context. It can't get the object at all. You need to wait until the save completes.
A couple of things you might try, depending on how your app is structured:
Use performBlockAndWait instead of performBlock, so that you'll know that the save has completed by the time the block finishes.
Keep using performBlock but put calls inside that block that lead to accessing the object on a different context-- via a nested performBlock call on that other context.

Resources