Updating CoreData Entity or saving a new one - ios

I'm fairly new to CoreData, and I'm trying to make a game. I have a couple of questions I was hoping you guys could help me out with some guidance:
- does GameKit already have some sort of CoreData integrated in it? I am not sure if I am overthinking this CoreData stuff if there's already something that replaces it in GameKit.
. . .
Anyways, assuming the answer to the above question is "no. GameKit has nothing to save your game". I will proceed with my current "Save game" code which is the following:
func saveCurrentMatch()
{
/* CORE DATA STUFF:
FIRST NEED TO VERIFY IF THIS GAME HAS BEEN PREVIOUSLY SAVED, IF SO THEN UPDATE, ELSE JUST SAVE
Entity: MatchData
Attributes: scoreArray (String), playerArray (String), myScore (Int), matchID (Int), isWaiting (Bool), isRealTime (Bool), gameLog (String)
*/
let context = myAppDelegate.persistentContainer.viewContext
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "MatchData")
request.returnsObjectsAsFaults = false
do
{
let gamesInProgress = try context.fetch(request)
print (gamesInProgress.count)
if gamesInProgress.count > 0 //HERE CHANGE THIS TO LOOK FOR THE MATCH ID OF THIS GAME!!
{
gameExistsinCD = true
}
else
{
gameExistsinCD = false
}
}
catch
{
print ("Error Reading Data: \(error.localizedDescription)")
}
if gameExistsinCD
{
//CODE TO UPDATE MATCH INSTEAD OF SAVING NEW ONE
}
else
{
// CODE TO SAVE A NEW MATCH FOR THE FIRST TIME
let matchData = NSEntityDescription.insertNewObject(forEntityName: "MatchData", into: context)
matchData.setValue(isRealTime, forKey: "isRealTime")
matchData.setValue(currentScore?[0], forKey: "myScore")
matchData.setValue(currentScore?.map{String($0)}.joined(separator: "\t"), forKey: "scoreArray") // IS THIS CODE CORRECT? I'M TRYING TO SAVE AN ARRAY OF INTS INTO A SINGLE STRING
matchData.setValue(currentPlayers?.joined(separator: "\t"), forKey: "playerArray")
matchData.setValue(true, forKey: "isWaiting") //will change later to update accordingly.
matchData.setValue(matchID, forKey: "matchID")
matchData.setValue(gameLog, forKey: "gameLog")
do
{
try context.save()
print ("CoreData: Game Saved!")
}
catch
{
print ("Error Saving Data: \(error.localizedDescription)")
}
}
}
My main concern is on the fetch request, how do I check all the core-data if this match has already been saved? and if so, whats the code for updating an Entity instead of inserting a new one?
Any guidance is appreciated, thanks!

Don't let Core Data scare you. It can be a fine way to save local data and despite some comments, it is not slow when done right. In fact, Core Data can be quite fast.
You can simplify your code a lot by using your Object class in a more normal fashion instead of using setValue calls. Your create code can be changed to this:
// CODE TO SAVE A NEW MATCH FOR THE FIRST TIME
if let matchData = NSEntityDescription.insertNewObject(forEntityName: "MatchData", into: context) as? MatchData {
matchData.isRealTime = isRealTime
matchData.myScore = currentScore?[0]
matchData.scoreArray = currentScore?.map{String($0)}.joined(separator: "\t") // IS THIS CODE CORRECT? I'M TRYING TO SAVE AN ARRAY OF INTS INTO A SINGLE STRING
// You can certainly save it this way and code it in and out. A better alternative is to have a child relationship to another managed object class that has the scores.
matchData.playerArray = currentPlayers?.joined(separator: "\t")
matchData.isWaiting = true
matchData.matchID = matchID
matchData.gameLog = gameLog
}
This is a much more readable and normal way to set your object properties. Any time you change a property on a core data managed object then it will get saved the next time you save the context.
As far as finding a current record that matches the ID, I like to add classes like that to my Managed Object class itself:
class func findByID(_ matchID: String) -> MatchData? {
let myAppDelegate = UIApplication.shared.delegate as! AppDelegate
let context = myAppDelegate.persistentContainer.viewContext
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "MatchData")
let idPredicate = NSPredicate(format: "matchID = \(matchID)", argumentArray: nil)
request.predicate = idPredicate
var result: [AnyObject]?
var matchData: MatchData? = nil
do {
result = try context.fetch(request)
} catch let error as NSError {
NSLog("Error getting match: \(error)")
result = nil
}
if result != nil {
for resultItem : AnyObject in result! {
matchData = resultItem as? MatchData
}
}
return matchData
}
Then any place you need the match data by ID you can call the class function:
if let matchData = MatchData.findByID("SomeMatchID") {
// Match exists
}

Core data is basically wrapper around SQL database. It is very efficient when you are working with high volume of data that need to be stored. So please consider either you had such requirements, otherwise perhaps it can be wise to store data in user defaults, or settings.
If it is, there is few things you need to know.
It is very useful to create you own model classes. Open core data model file, open "Editor/Create NSManagedObject subclass". It will allow you to refer direct properties, instead of KVC(setValue:forKey:).
Alway mind what thread you are working in. It is unsafe to work with objects, created in other threads.
Your gamesInProgress contains array of objects you fetched from your database.
So basically instead of
if gameExistsinCD
{
//CODE TO UPDATE MATCH INSTEAD OF SAVING NEW ONE
}
else
{
// CODE TO SAVE A NEW MATCH FOR THE FIRST TIME
let matchData = NSEntityDescription.insertNewObject(forEntityName: "MatchData", into: context)
matchData.setValue(isRealTime, forKey: "isRealTime")
<...>
you can do
let matchData = (gamesInProgress.first ??
NSEntityDescription.insertNewObject(forEntityName: "MatchData", into: context)) as! <YouEntityClass>
matchData.isRealTime = isRealTime
<...>
PS: https://www.raywenderlich.com/173972/getting-started-with-core-data-tutorial-2

Related

NSBatchInsertRequest fetch objects after its completed

I'm using a batch insert request like this:
let insertRequest = NSBatchInsertRequest(entity: EntryObject.entity(), managedObjectHandler: { object in
guard index < total else { return true }
if let entryObject = object as? EntryObject {
let entry = entries[index]
entryObject.setValues(entry: entry, shouldAttemptFetchExistingTags: true)
}
index += 1
return false
})
try context.execute(insertRequest)
Each EntryObject needs to be related to several other entities. However, NSBatchInsertRequest doesn't allow creating relationships. Each object is saved directly to the store without loading it into a context.
So my plan is to fetch each of these objects afterwards and apply the necessary relationships.
I'm using a batch insert request because EntryObject can contain several movies that are expensive as crap to create, and I'm continually getting super hard to trace crashes.
However when I go back to fetch these objects.. they don't exist.
I tried storing the objectIDs in a temporary array. That threw the same error.
Then I tried manually merging the changes into the context I performed the batch insert on, in addition to the viewContext.
insertRequest.resultType = NSBatchInsertRequestResultType.objectIDs
let result = try context.execute(insertRequest) as! NSBatchInsertResult
if let objectIDs = result.result as? [NSManagedObjectID], !objectIDs.isEmpty {
let save = [NSInsertedObjectsKey: objectIDs]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: save, into: [PersistenceController.viewContext, context])
}
And when that didn't work, I watch the 2018 best practices for CoreData and got this going..
try context.performAndWait {
let request = NSPersistentHistoryChangeRequest ()
request.resultType = .transactionsAndChanges
let result = try context.execute (request) as! NSPersistentHistoryResult
let transactions = result.result as! Array<NSPersistentHistoryTransaction>
for transaction in transactions {
context.mergeChanges(fromContextDidSave: transaction.objectIDNotification())
}
}
try context.save()
It's supposed to merge all the changes to the store into the main context.
However when I go back to fetch these objects, on either context.. they don't exist. Not until I kill the app and start over.
I've been trying to figure out how to completely reinit a store.. but have had no success.
How do I fetch these objects right after I create them using an NSBatchInsertRequest?

Is it possible to perform NSBatchUpdateRequest with an array of NSManagedObjectID?

Currently, I perform multiple update operations via the following code.
func updateOrders(_ updates : [(objectID: NSManagedObjectID, order: Int64)]) {
if updates.isEmpty {
return
}
let coreDataStack = CoreDataStack.INSTANCE
let backgroundContext = coreDataStack.backgroundContext
backgroundContext.perform {
for update in updates {
let objectID = update.objectID
let order = update.order
let nsPlainNote = try! backgroundContext.existingObject(with: objectID) as! NSPlainNote
nsPlainNote.order = order
}
RepositoryUtils.saveContextIfPossible(backgroundContext)
}
}
Since I would like to
Make the update operations run faster
Avoid delegate of NSFetchedResultController from being notified
I would like to utilise NSBatchUpdateRequest for performing such update operation.
However, I don't find a way, how I can apply array of NSManagedObjectID and array of Int64 value, to NSBatchUpdateRequest.
Given an array of NSManagedObjectID and Int64, is it possible to use NSBatchUpdateRequest to perform updated on CoreData?
You must use NSPredicate to set object id
func updateOrders(_ updates : [(objectID: NSManagedObjectID, order: Int64)]) {
updates.forEach {
let request = NSBatchUpdateRequest(entityName: "NSPlainNote")
request.propertiesToUpdate = ["order": $0.order]
request.predicate = NSPredicate(format: "objectID == %#", $0.objectID)
let result = try? context.execute(request)
}
}
NSBatchUpdateRequest is not suitable for your task since using it makes sense for large amount of records with a common attribute's value so that you can filter all by your criteria and update all fields with your values at once.
The fact is that the NSBatchDeleteRequest is an NSPersistentStoreRequest which operates at the SQL level in the persistent store itself and it doesn't update your in-memory objects after execution thats why it works so fast and Core Data translates your native requests to a SQL ones where you can not use dynamically code to get and insert needed data from dictionary etc. but you can update the current value of a filed e.g.:
let batchRequest = NSBatchUpdateRequest(entityName: "Note")
batchRequest.predicate = predicate
// Increase `order` value
batchRequest.propertiesToUpdate = ["order" : NSExpression(format: "order + 1")]
do {
try context.execute(batchRequest)
}
catch {
fatalError(error.localizedDescription)
}

Core data - Set many-to-one relationship with NSBatchInsertRequest

I have two entities, User and Post. A User contains many Post, and each Post has only one User.
I am trying to batch insert more than 1000 posts.
private func newBatchInsertRequest(with posts: [PostData]) -> NSBatchInsertRequest {
var index = 0
let total = posts.count
let batchInsert = NSBatchInsertRequest(entity: Post.entity()) { (managedObject: NSManagedObject) -> Bool in
guard index < total else { return true }
if let post = managedObject as? Post {
let data = posts[index]
post.createdData = data.createdDate
post.identifier = data.identifier
post.text = data.text
}
index += 1
return false
}
PersistenceController.shared.container.performBackgroundTask { context in
try? context.execute(batchInsert)
try? context.save()
}
}
It inserts all the posts that I want to insert. However, I can not configure how to set their User.
I tried to use the following code, but it did not work.
let updateRequest = NSBatchUpdateRequest(entity: Post.entity())
updateRequest.resultType = .updatedObjectIDsResultType
updateRequest.propertiesToUpdate = ["user": user]
try? context.execute(updateRequest)
I get the following error.
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Invalid relationship ((<NSRelationshipDescription: 0x2801045a0>), name item, isOptional 1, isTransient 0, entity Post, renamingIdentifier item
I can set their user one by one, but it is inefficient due to long processing time.
How to update the user property of Posts in a more efficient way?
Update
All of these posts belongs to one User which does not exist yet. I need to create it before or after executing the NSBatchInsertRequest.
User has three properties
1. createdDate: Date
2. identifier: UUID
3. name: String
My goal is to insert Post that belongs to one Use either using NSBatchInsertRequest or in a private context so that it does not block the main thread.
Here's what I would try:
Create the User before your batch operation. Yep, it's going to be on a different context — just make sure you grab its NSManagedObjectID (it's a property on NSManagedObject) and save it on a private property.
Inside your PersistenceController.shared.container.performBackgroundTask you have a reference to your private context. Use the managed object ID from before to fetch/register the same User in your private context:
let user = context.object(with: userManagedObjectID)
Then pass this user into the method that creates your batch insert request to set the user property on your Post objects.
I believe if you set up your inverse relationship up in the data model editor you don't need to populate User.posts. That should happen automatically for you.
I solved this problem in different way.
In my entity which needs to have relationship added placeholder attributed tmpID.
When creating batch inserts setting tmpID to particular string. As example parent objectID parent.objectID.
Executing batch insert.
Then fetching all object from CoreData with tmpID.
Then going throw everyone and setting:
Relationship
tmpID to nil, the do not waist a memory.
Saving managed object context.
To merge I am using function:
/// Executes the given `NSBatchDeleteRequest` and directly merges the changes to bring the given managed object context up to date.
///
/// - Parameter batchDeleteRequest: The `NSBatchDeleteRequest` to execute.
/// - Throws: An error if anything went wrong executing the batch deletion.
public func executeAndMergeChanges(using batchDeleteRequest: NSBatchDeleteRequest) throws {
batchDeleteRequest.resultType = .resultTypeObjectIDs
let result = try execute(batchDeleteRequest) as? NSBatchDeleteResult
let changes: [AnyHashable: Any] = [NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [self])
}
// Use case:
do {
try container.viewContext.executeAndMergeChanges(using: deleteReqest)
try container.viewContext.save()
} catch {
Logger.app.e("Can not destroy/save clean table: \(name) -> \(error)")
}

SWIFT: When mapping Firebase info to an object the objects are not being added to the array once returned, why?

Hi I am new to Swift/Firebase and am a bit clueless as to why cities is empty after returning objects to it. I have stepped through and a new City is being created but after it does all of the City objects, cities shows as empty. What could be causing this? The idea is to retrieve the name and image (from Firebase Storage), create a new City object from them and then have an array of these objects that I can access from another class.
self.cities = documents.map { queryDocumentSnapshot-> City in
let data = queryDocumentSnapshot.data()
let cityName = data["name"] as? String ?? ""
var cityImage: UIImage = UIImage()
let httpsRef = Storage.storage().reference(forURL: (data["image"] as! String))
httpsRef.getData(maxSize: 1 * 1024 * 1024) { data, error in
if let error = error {
print("ERROR!!! \(error)")
} else {
cityImage = UIImage(data: data!)!
}
}
return City(name: cityName, image: cityImage)
}
httpsRef.getData() is asynchronous and returns immediately, before the object data is available. The callback you provide is invoked some time later, after the data is fetched. Meanwhile, your map closure goes on to return a new City object with the initial value of cityImage that you provided before the call to getData(). Your code will need to be rewritten to account for the asynchronous nature of getData(), possibly executing in multiple stages.

Core Data model design or architecture

I'm planning to use Core Data to store my app's data. However, when I tried to design the architecture of the model entities, I found that the most core data tutorials mixed the "fetch data"、"insert data"、"delete data" with ViewControllers. Doesn't such operations be put in model's implementation according to the information system design principle??
For example, there is a base ModelClass:
class BaseModel {
var modelName: String!
// insert
func insertNewObjectToModel(managedContext:NSManagedObjectContext)->BaseModel {
return NSEntityDescription.insertNewObjectForEntityForName(modelName, inManagedObjectContext: managedContext) as! BaseModel
}
// get
func fetchObject(predicate: NSPredicate)...
// delete
func deleteObject(object:BaseModel)...
}
And then I have a model "Student" inherit from BaseModel class, so it inherits its methods of inserting, querying, deleting and so on. Then in ViewController class, if user wants to add a new object, just call student.insertNewObject(context).
I'm really confused about whether such design or encapsulation works. I have read Ray's 《Core Data by Tutorials》, and also read the Apple's Core Data examples. But it seemed that they all put model related operations in ViewController.
For example, I picked a few lines of code from Ray's book:
func populateDealsCountLabel() {
// 1
let fetchRequest = NSFetchRequest(entityName: "Venue")
fetchRequest.resultType = .DictionaryResultType
// 2
let sumExpressionDesc = NSExpressionDescription()
sumExpressionDesc.name = "sumDeals"
// 3
sumExpressionDesc.expression = NSExpression(forFunction: "sum:", arguments: [NSExpression(forKeyPath: "specialCount")])
sumExpressionDesc.expressionResultType = .Integer32AttributeType
// 4
fetchRequest.propertiesToFetch = [sumExpressionDesc]
// 5
do {
let results = try coreDataStack.context.executeFetchRequest(fetchRequest) as! [NSDictionary]
let resultDict = results.first!
let numDeals = resultDict["sumDeals"]
numDealsLabel.text = "\(numDeals!) total deals"
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
}
}
English is not my tongue language, so I'm not sure if you seeing the question understand what my description want to express. But I'm hoping for your comments and answers! Thanks in advance!

Resources