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!
Related
I've used core data to store 10000-20000 records. if i try to save and fetch 10000 records memory and cpu consumption was huge due to that app is getting crash in iphone 6 plus and earlier devices.
Here is the saving methods:----
//InboxCoredata Saving Method i..(calling saving method ii)
func InboxSaveInCoreDataWith(array: [[String: AnyObject]])
{
_ = array.map{self.InboxCreateCollectionEntityFrom(dictionary: $0)}
do
{
try CoreDataStack.sharedInstance.persistentContainer.viewContext.save()
print("Inbox Data saved Sucessfully in Coredata ")
}
catch let error
{
print(error)
}
}
// Inbox Coredata saving method ii
func InboxCreateCollectionEntityFrom(dictionary: [String: AnyObject]) -> NSManagedObject?
{
let context = CoreDataStack.sharedInstance.persistentContainer.viewContext
if let inboxEntity = NSEntityDescription.insertNewObject(forEntityName: "InboxData", into: context) as? InboxData {
inboxEntity.fileType = dictionary["FileType"] as? String
inboxEntity.sender = dictionary["Sender"] as? String
inboxEntity.mailPath = dictionary["MailPath"] as? String
inboxEntity.fullMail = dictionary["FullMail"] as? NSObject
inboxEntity.attachmentName = dictionary["AttachmentName"] as? String
inboxEntity.size = dictionary["Size"] as! Int32
inboxEntity.date = dictionary["Date"] as? NSDate
inboxEntity.dateForSearch = dictionary["DateForSearch"] as? String
inboxEntity.inboxMail = dictionary["InboxMail"] as? String
return inboxEntity
}
return nil
}
And Here is the method for fetching:----
strongSelf.inboxDataFromCoreData(fetchLimit: 0) // Methods calling in viewdidload
//MARK: - Fetching inboxData from Coredata
func inboxDataFromCoreData(fetchLimit :Int)
{
var inboxCoredataFetch = [[String : AnyObject]]()
let context = CoreDataStack.sharedInstance.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: String(describing: InboxData.self))
do {
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]
fetchRequest.fetchLimit = fetchLimit
let results = try context.fetch(fetchRequest) as! [InboxData]
print("inbox_coredata:\(results .count)")
for data in results
{
let sender = data.sender
let mailPath = data.mailPath
let fileType = data.fileType
let fullMail = data.fullMail
let attachmentName = data.attachmentName
let size = data.size
let date = data.date
let dateForsearch = data.dateForSearch
let inboxMail = data.inboxMail
inboxCoredataFetch.append(["Sender" : sender as AnyObject,"MailPath": mailPath! as AnyObject, "FileType":fileType as AnyObject, "FullMail":fullMail as AnyObject, "AttachmentName": attachmentName as AnyObject, "Size":size as AnyObject,"Date": date as AnyObject,"DateForSearch" :dateForsearch as AnyObject,"InboxMail":inboxMail as AnyObject])
}
}
catch let err as NSError {
print(err.debugDescription)
}
var sortingdata = inboxCoredataFetch as Array
let mailBoxSortDescriptor = NSSortDescriptor(key: "Date", ascending:false, selector: nil)
let dateDescriptor = NSSortDescriptor(key: "AttachmentName", ascending: true, selector: #selector(NSString.caseInsensitiveCompare))
sortingdata = ((sortingdata as NSArray).sortedArray(using: [ mailBoxSortDescriptor,dateDescriptor]) ) as! [[String : AnyObject]]
inboxTotalMailData = sortingdata
if appDataLoadingFirst == true
{
appDataLoadingFirst = false
displayTotalData = sortingdata
DispatchQueue.main.async {
self.hideActivityIndicator()
self.collectionView.reloadData()
}
}
}
Core data structure is like this:::---
i've too much confusion questions on these.
Core data is not good for storing 20000 records??
Do i need to refreshObject(object,mergeChnage:bool) for huge data
everytime i've to refresh object as like that.
Instruments indicates memory leak in fetching results at let results =
try context.fetch(fetchrequest.....line why??
Do i need to save and fetch data in batches will that increases app
performence and memory reduce??
Why CPU indicates 100% sometimes ??
if i display 10000 records in collection view(data loading from arrray)
causes any issue?? if yes what kind of issue??
Need your valuable suggesstion and help to make me perfect????
You're using a lot of memory and CPU time because:
When you're creating new InboxData entries, you go create one for every single entry in the array before you save changes. Your code means that all of those must be in memory at the same time. If you have thousands of entries, that's a lot of memory.
When you fetch entries, you fetch every single entry every time. Again, your code means that you must have all of them in memory at the same time. And again, lots of entries mean lots of memory. One of the main reasons to use Core Data is that it's almost always possible to load only a subset of your data into memory, instead of everything. You're loading everything.
You copy a bunch of attributes from managed objects into dictionaries-- so now you have two copies of the data in memory instead of one.
Your code that fetches data sorts the data in memory to create sortingdata. You already have a ton of objects loaded into memory; now you're doing a ton of work to sort them all. This will be at least part of the reason you peg the CPU at 100%.
After sorting the data you assign the result to inboxTotalMailData and to displayTotalData, which are defined somewhere outside this function. This means that all of the data in sortingdata remains in memory even after this function finishes.
Some things that would help:
When saving data, save at regular intervals-- every 50 entries, or every 100, or whatever number gives good results. Do not attempt to create many thousands of objects and keep them all in memory.
Have the fetch request sort the data instead of fetching and then sorting. Fetch requests can have sort descriptors, and the sorting is done by SQLite. This will be far more efficient in both CPU time and memory use.
Try to avoid fetching everything at once, because with thousands of records that means a lot of memory, no matter what else you do.
Don't copy the data unless you have some strongly compelling reason to do so. You can use managed objects directly, it's almost never appropriate to duplicate the data like this.
Since you appear to be using a collection view, consider using NSFetchedResultsController. It's designed to work with table and collection views and will help keep memory and CPU use down.
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
I have a tableView and that tableview is being populated with data from Coredata. The data breaks down like this.
Entity - Person
Entity - Statement
The statement entity has an attribute called amountOwed and it is of decimal type.
The relationships is that a person can have many statements, but each statement belongs to a single person.
Here is the path of the data that I would like to add up. Person > Statement > AmountOwed.
In the tableView function I have a let that represents the Person entity.
let person = fetchedResultsController.object(at: indexPath)
I know its working because I can print out the persons name like so
print(person.name) // Bob
What I want to be able to do is add up all the amountOwed attributes for each Person inside the Statement entity and display them on a cell.
I have been trying to follow an example of calculated fetches but I seem to not quiet understand how to target my Statements Entities which are linked to each Person entity.
let fetchRequest = NSFetchRequest<NSDictionary>(entityName:"statement")
fetchRequest.resultType = .dictionaryResultType
let sumExpressionDesc = NSExpressionDescription()
sumExpressionDesc.name = "sumDeals"
let specialCountExp = NSExpression(forKeyPath: #keyPath(Person.statement[indexPath].amountOwed))
sumExpressionDesc.expression = NSExpression(forFunction: "sum:", arguments: [specialCountExp])
sumExpressionDesc.expressionResultsType = .interger32AttributeType
fetchRequest.propertiesToFetch = [sumExpressionDesc]
do{
let results = try coreDataStack.managedContext.fetch(fetchRequest)
let resultDict = results.first!
let numDeals = resultDict["sumDeals"]
print(numDeals!)
}
catch let error as NSError{
print("Count not fetched \(error), \(error.userInfo)")
}
Do I need to fetch a Statement entity or should I just use the FetchedREsultsController? If I do use my fetchedResultsController does the keypath to the Statement Entity look like this
person[indexPath].statement.amountOwed
You can do that in one line. If the relationship from Person to Statement is called statements, you get the total of the amounts with
let amountTotal = newPerson.value(forKeyPath: "statements.#sum.amount") as? Int64
Change the downcast at the end from Int64 to whatever is appropriate for your amount attribute-- Double or whatever.
OK on your People+CoreDataClass add:
var totalOwed: Float {
get {
var value: Float = 0
if let statements = self.statements.allObjects() as? [Statement] {
for s in statements {
value = value + s.sum
}
}
return value
}
}
And remove all the code from your fetch that is unnecessary
I have been unable to add and read core data attributes in a one-to-many
relationship. After reading many SO, Apple Docs and other articles I still have
not accomplished this. To make a simple test, I created a standard Master Detail
App with Core Data. This works with no issues for the main entity.
The core data relationship is as shown below.
Here is the code to place some test data into the store. Note that I am
attempting to add the to-many data as a Set for each InvItem and have made the
keywordList attribute Transformable. I thought this was the best approach, but
obviously that is not working here.
func seedInvItems(num : Int) {
for index in 1...num {
let newManagedObject = NSEntityDescription.insertNewObjectForEntityForName("InvItem", inManagedObjectContext: kAppDelegate.managedObjectContext) as! InvItem
newManagedObject.name = "myName\(index)"
newManagedObject.category1 = "myCategory1x\(index)"
newManagedObject.compartment = "myCompartment\(index)"
newManagedObject.entryDate = NSDate()
//and for the one-to-many relationship
var myStoreKeywordSet = Set<String>()
myStoreKeywordSet = ["one","two","three"]
// do an insert just for grins
myStoreKeywordSet.insert("four")
let newManagedObject2 = NSEntityDescription.insertNewObjectForEntityForName("InvItemKeyword", inManagedObjectContext: kAppDelegate.managedObjectContext) as! InvItemKeyword
newManagedObject2.keywordList = myStoreKeywordSet
}//for in
kAppDelegate.saveContext()
let fetchRequestTest = NSFetchRequest(entityName: "InvItem")
let sorter : NSSortDescriptor = NSSortDescriptor(key: "name", ascending: true)
fetchRequestTest.sortDescriptors = [sorter]
var resultsTest : [InvItem]?
do {
resultsTest = try kAppDelegate.managedObjectContext.executeFetchRequest(fetchRequestTest) as? [InvItem]
for object in resultsTest! {
let myRetrieveKeywordSet = object.invitemkeywords
print(myRetrieveKeywordSet)
}//for in
} catch let error as NSError {
//what happens on failure
print("And the executeFetchRequest error is \(error.localizedDescription)")
}//do catch
}//seedInvItems
For completeness, I did create the NSManagedObject subclasses for the Core
Data Entities.
When running the app, the master detail for InvItem behaves as expected but I
get no storage of the to-many items.
Here is the console log (for one InvItem):
Optional(Relationship 'invitemkeywords' fault on managed object (0x7f985a62bb70) (entity: InvItem; id: 0xd000000000700000 ; data: {
category1 = myCategory1x3;
compartment = myCompartment3;
entryDate = "2016-02-09 02:10:21 +0000";
invitemkeywords = "";
name = myName3;
}))
Looking at the database, there is no data for the keywordList.
Any help would be appreciated. Xcode 7.2.1 IOS 9.2
You need to set the relationship from newManagedObject to newManagedObject2. In fact, because the relationship is one-many, it is easier to set the to-one relationship:
newManagedObject2.invitem = newManagedObject
and let CoreData handle the inverse, to-many relationship.
As to the keyWordList not being populated, I wonder whether your SQL browser is unable to decode CoreData's NSSet? I also wonder whether you need to have keywordList as an NSSet? The relationship from InvItem is already to-many, so your model implies that each InvItem can have many InvItemKeywords, each of which holds many keywords in its keywordList attribute: is that what you intended?
When trying to downcast a fetched NSManagedObject to it's correct subclass StoryPhotoTrack in my (Swift) unit tests, I get a runtime error: Could not cast value of type 'StoryPhotoTrack_StoryPhotoTrack_' (0x7fbda4734490) to 'StoryPhotoTrack' (0x11edd1b08).
Here's my failing / crashing test:
func testFetchingStoryWithCastingInvolved() {
let story : StoryPhotoTrack? = storyFetcher.fetchAnyStoryPhotoTrack()
XCTAssertNotNil(story, "should be a story") // works
let definitlyAStory : StoryPhotoTrack = story! // works
let object : NSManagedObject = story as NSManagedObject! // works
println(NSStringFromClass(definitlyAStory.dynamicType)) // prints 'StoryPhotoTrack_StoryPhotoTrack_'
let downCastedStory : StoryPhotoTrack = object as! StoryPhotoTrack // -> error: "Could not cast..."
XCTAssertNotNil(downCastedStory, "should still be a story")
}
// StoryFetcher.swift
func fetchAnyStoryPhotoTrackVia_ObjectiveC_Fetcher() -> StoryPhotoTrack? {
let object = StoryPhotoTrack.fetchAnyStoryPhotoTrackInContext(moc)
return object as StoryPhotoTrack?
}
The actual fetching is done in Objective-C:
// StoryPhotoTrack+Fetching.m
+ (StoryPhotoTrack *)fetchAnyStoryPhotoTrackInContext:(NSManagedObjectContext *)moc
{
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"StoryPhotoTrack"];
NSArray *storyTracks = [moc executeFetchRequest:request error:nil];
return (StoryPhotoTrack *)[storyTracks lastObject];
}
I assume, it has something to do with Swift's namespaces. Seems like my StoryPhotoTrack class is actually called StoryPhotoTrack_StoryPhotoTrack_.
I tried renaming the class names of my core data entities with projectName-prefixes (as suggested here), but that did not help. I tried both my main target module name and my test target module name, none of them worked.
What am I doing wrong?
edit: seems like I overlooked SO questions like How to test Core Data properly in Swift, so it seems, this issue is known, but not easy to solve. Is it really the best way to create a separate framework for your Core Data Model?
I had the same issue, for some reason, I was not able to cast to my custom NSManagedObjects for unit test, the only solution was to keep the result set as NSManagedObject Array and access it is fields as below without casting it to ProductModel, object in itself is still a ProductModel but casting it cause an error.
I kept my model name as ProductModel without MyAppName.ProductModel.
func testProductModel() {
let resultFetchRequest = NSFetchRequest(entityName: ProductModel.Name)
let productModels: [NSManagedObject] = context.executeFetchRequest(resultFetchRequest, error: &error) as!
[NSManagedObject]
if let recipes = productModels[0].valueForKey("recipes") as? NSSet {
let recipesCount = recipes.count;
XCTAssertTrue(recipesCount == 0)
} else {
XCTFail("FAIL - no recipes nsset")
}
}