I have defined a Parse model class Task which contains a bunch of simple data (text, images), and can optionally represent a composite structure via a parent/child relation, implemented by means of a parent pointer and a children array:
class Task: PFObject, PFSubclassing {
class func parseClassName() -> String { "Task" }
#NSManaged var note: String?
#NSManaged var images: [PFFileObject]?
#NSManaged var parent: Task?
#NSManaged var children: [Task]?
}
Using Xcode's "Debug Memory Graph" view, I observed that this data structure causes a memory leak: when parent/child Task objects are released by my code, they still are kept in memory. This does not happen for "standalone" Task objects (i.e. the ones having neither a parent or children), they get released correctly.
I already tried to manually unset the parent/child relation properties, without success:
for task in tasks {
for childTask in task.children ?? [] {
childTask.parent = nil
// or: childTask.remove(forKey: "parent")
}
task.children = []
// or: task.remove(forKey: "children")
}
I still assume that the problem comes from the strong reference in the children property; for some reason, I cannot break it by manually unsetting that reference though. Any help resolving the memory leak in this data model is appreciated :)
(P.S.: I know about relations; please don't suggest to change the data model in this direction.)
Using Parse-SDK-iOS-OSX version 1.19.4 via CocoaPods, Swift, Xcode 14.2, iOS 16.2.
Related
I am in the beginning stages of developing an open-source utility for storing state in the Bundle UserDefaults.
I'm encountering an issue when I add non-Codable data types to my Dictionary of [String: Any].
I need to be able to vet the data before trying to submit it, because the UserDefaults.set(_:) method won't throw any errors. It just crashes.
So I want to make sure that the Dictionary that I'm submitting is kosher.
I can't just check if it's Codable, because it can sometimes say that it isn't, when the struct is actually good. (It's a Dictionary<String, Any>, and I can cram all kinds of things in there).
I need to validate that the Dictionary can produce a plist. If this were ObjC, I might use one of the NSPropertyListSerialization methods to test the Dictionary, but it appears as if this set of methods is not available to Swift.
According to the UserDefaults docs, there are a specific set of types and classes that are "plist-studly."
I think testing each type in the list is unacceptable. I need to see if I can find a way to test that won't be screwed the first time Apple updates an OS.
Is there a good way to test a Dictionary<String, Any> to see if it will make UserDefaults.set(_:) puke?
The Property List type set of UserDefaults is very limited. The supported types are
NSString → Swift String
NSNumber → Swift Int, Double or Bool
NSDate → Swift Date
NSData → Swift Data
Arrays and dictionaries containing the 4 value types.
Any is not supported unless it represents one of the 4 value or 2 collection types.
Property List compliant collection types can be written to UserDefaults with PropertyListSerialization (even in Swift).
There are two protocols to serialize custom types to Data
Codable can serialize structs and classes.
NSCoding can serialize subclasses of NSObject.
All types in the structs/classes must be encodable and decodable (means conform to the protocol themselves).
The APIs of PropertyListSerialization / PropertyListEncoder/-Decoder and NSKeyed(Un)Archiver provide robust error handling to avoid crashes.
UPDATE[1]: And, just because I like to share, here's the actual completed project (MIT License, as is most of my stuff)
UPDATE: This is the solution I came up with. Even though I greenchecked vadian's excellent answer, I decided to get a bit more picky.
Thanks to matt pointing out that I was looking under the wrong sofa cushions for the keys, I found the Swift variant of NSPropertyListSerialization, and I use that to vet the top level of the tree. I suspect that I'll need to refactor it into a recursive crawler before I'm done, but this works for now.
Here's the code for the _save() method at the time of this writing. It works:
/* ################################################################## */
/**
This is a private method that saves the current contents of the _values Dictionary to persistent storage, keyed by the value of the "key" property.
- throws: An error, if the values are not all codable.
*/
private func _save() throws {
#if DEBUG
print("Saving Prefs: \(String(describing: _values))")
#endif
// What we do here, is "scrub" the values of anything that was added against what is expected.
var temporaryDict: [String: Any] = [:]
keys.forEach {
temporaryDict[$0] = _values[$0]
}
_values = temporaryDict
if PropertyListSerialization.propertyList(_values, isValidFor: .xml) {
UserDefaults.standard.set(_values, forKey: key)
} else {
#if DEBUG
print("Attempt to set non-codable values!")
#endif
// What we do here, is look through our values list, and record the keys of the elements that are not considered Codable. We return those in the error that we throw.
var valueElementList: [String] = []
_values.forEach {
if PropertyListSerialization.propertyList($0.value, isValidFor: .xml) {
#if DEBUG
print("\($0.key) is OK")
#endif
} else {
#if DEBUG
print("\($0.key) is not Codable")
#endif
valueElementList.append($0.key)
}
}
throw PrefsError.valuesNotCodable(invalidElements: valueElementList)
}
}
this is my first time working with Core Data in swift. I'm really enjoying it but it's also a challenge making sure my Appdelegate saves etc.
The Problem
Basically I am creating an budgeting app. Once a budget ends I need to take the current budget and store it away into a history entity. Now I have 2 different entities that work here:
NewBudgetCreateMO and HistoryBudgetHolderMO. What should happen is that the HistoryBudgetHolder should add a budget (newBudgetCreateMO) into it's One-To-Many relationship. Here is an image of my graph and their relationship.
Now if I've set this up right I should be allow to have as many NewBudgetCreateMOs in my History as I like by adding them? The code below is the generated code for my History entity which shows that it contains an NSSet
extension HistoryBudgetHolderMO {
#nonobjc public class func fetchRequest() -> NSFetchRequest<HistoryBudgetHolderMO> {
return NSFetchRequest<HistoryBudgetHolderMO>(entityName: "HistoryBudgetHolder");
}
#NSManaged public var budgets: NSSet?
}
extension HistoryBudgetHolderMO {
#objc(addBudgetsObject:)
#NSManaged public func addToBudgets(_ value: NewBudgetCreateMO)
#objc(removeBudgetsObject:)
#NSManaged public func removeFromBudgets(_ value: NewBudgetCreateMO)
#objc(addBudgets:)
#NSManaged public func addToBudgets(_ values: NSSet)
#objc(removeBudgets:)
#NSManaged public func removeFromBudgets(_ values: NSSet)
}
So I assumed that I could just use "addToBudgets" to add a set piece of data and it does seem to work but for only one instance.
Where I'm doing the adding
So I do a fetch request on the HistoryBudgetHolderMO to see if I have any in the data base. If not then I create a new one from my App Delegate (Please NOTE: I have done the app delegate casting etc in a method above and then have passed the App Delegate and Context to this method)
private func SaveAndDeleteCurrentBudget(context : NSManagedObjectContext, appDele : AppDelegate){
let fetchHistory : NSFetchRequest<HistoryBudgetHolderMO> = HistoryBudgetHolderMO.fetchRequest()
//Saves the budget to the history budget. If we don't have oen we created one and add it to that
do{
let historyBudgets : [HistoryBudgetHolderMO] = try context.fetch(fetchHistory)
if historyBudgets.count <= 0{
let newHistoryBudget : HistoryBudgetHolderMO = HistoryBudgetHolderMO(context: context)
newHistoryBudget.addToBudgets(budgetData.first!)
print("entered new historyBudget")
}else{
historyBudgets.first!.addToBudgets(budgetData.first!)
}
appDele.saveContext()
}catch{
print("Error when looking for history fetch result")
}
//Deletes all budget data and budget entries that are currently used
for object in budgetData{
context.delete(object)
}
let fetchAllDataEntries = NSFetchRequest<NSFetchRequestResult>(entityName: "BudgetEntry")
let deleteReq = NSBatchDeleteRequest(fetchRequest: fetchAllDataEntries)
do{
try context.execute(deleteReq)
}catch{
print("Error when deleting budget entries")
}
appDele.saveContext()
}
I do the fetch request and check if a history entity is there. If not then I create a new one, add the budget entry and then save the context.
If not then I grab the first instance of the history holder (as there should only ever be one as it's just a container) and I add the budget entry and then save.
Where it gets bad
So the first time I do this and it's in state 2 I get a value of Optional(1) which means it has stored one entry of the History. However any more additions after this keep saying it's Optional(1). I've tried looking up countless solutions, tried messing around with the extensions etc. I figured this would be a simple Get/Set operation but It's just not working.
Any help with this would be greatly appreciated.
Thanks for taking the time to read this.
Your solution seems good now. I also would have suggested to get rid of the HistoryBudgetHolderMO class. May I suggest to add another field/property to the NewBudget class: a creationDate (Date type). That way you can always fetch the latest one (e.g. fetch all of them and sort by creationDate). You could als add an active/historic boolean property to mark Budgets as active/inactive. Another suggestion is try to avoid force unwrapping. Instead of writing
budgetData.first!.attributeName
try to work with the 'if let' construct
if let budget = budgetData.first {
budget.attributeName
}
Solution For Anyone Interested
As I mentioned before I'm still learning Core Data and I'm grateful for KaraBenNensi for his comment to get me thinking.
Right so there was no need for a "holder" type object. Instead what I have done is I have used the last index of my budgets. So everytime I create a new budget I simply keep them all in the array. So instead of saying:
budgetData.first!.attributeName
I now use
budgetData.last!.attributeName.
This means that my database will grow but it would have grown with the history holder anyway. Now when I want to display history I just fetch all the results from the budgetData core data model. When I want to display my actual budget I just use .last so I get the most recently created budget.
I hope this helps someone and I'm glad I could figure it out. If anyone needs help in the future just reply to this and I'll try to help (But I'm no expert!)
I have coreData entities defined as
In TTFinds
#nonobjc public class func fetchRequest() -> NSFetchRequest<TTFinds> {
return NSFetchRequest<TTFinds>(entityName: "TTFinds");
}
I am trying to get TTFinds list as
let request: NSFetchRequest<TTFinds> = TTFinds.fetchRequest()
do {
let result = try appDel.persistentContainer.viewContext.fetch(request)
As can be seen TTFinds gets its parent TTArea object. However i want to get only few columns. Let's say name and creationDate only.How can it be done?
I have tried
let request: NSFetchRequest<TTFinds> = TTFinds.fetchRequest()
request.propertiesToFetch = NSArray.init(objects: "name", "creationDate", "image", "latitude", "longitude",
"area.name") as? [Any];
However area.name gives errors.
Forget propertiesToFetch and just fetch the entire object.
First, you can only retrieve selective attributes if you use a result type of dictionary. It will make your code more cumbersome and lengthy, without any gain in efficiency.
Second, if you just fetch the objects, even if there are a lot of them, Core Data will do all kinds of optimizations "under the hood" and just fetch what it needs. This is triggered by simply accessing the properties you need.
Thus, assuming your purpose for fetching only some properties is to conserve memory or reduce performance bottlenecks, don't worry about it until you actually hit those bottlenecks.
NB: your relationships should have inverse relationships!
I have a Core Data, called TaskManager, On this CoreData at the moment only one Entity, Task: Task got severals attributes:
import Foundation
import CoreData
#objc(Task)
class Task: NSManagedObject {
#NSManaged var context: String
#NSManaged var date: String
#NSManaged var detail: String
#NSManaged var id: String
#NSManaged var responsable: String
#NSManaged var status: String
#NSManaged var summary: String
}
If I add a new Entity , for example User:
import Foundation
import CoreData
#objc(User)
class User: NSManagedObject {
#NSManaged var id: String
#NSManaged var name: String
}
and I Reset contents and settings... from the emulator to let it know the new Entity, my application starts but I can't do anything, it's just frozen, can't click on buttons, the UITableView scroll doesn't work, when I stop the emulator to uninstall the application I can't, holding left click does nothing... I have to delete the Entity from my coredata, move to trash the NSManagedObject User auto generated by my Core Data, close the emulator, restart everything and there it works again, but what I need is to work with this other entity, User, so deleting it is... quite dumb..
How am I supposed to do to create the new Entity without freezing all the application?
Thank you for your help.
Regards.
Fselva
Each time you change your data model, you must uninstall your app on the device or on the simulator.
Thus, in the next build and run, the updated DB will be loaded. Of course, you lose all the data being loaded in the precedent version.
I would like to have synchronized http request results with my Core Data database. For example very basic class:
class Category: NSManagedObject {
#NSManaged var id: NSNumber
#NSManaged var name: String
#NSManaged var imageUrl: String
}
And I have this method for getting results from request:
apiManager.getRequestJsonParse(Constants.Server.BackendUrl + "categories?lang=cs", completion: { (result, error) -> Void in
completion(categories: [], error: error)
})
Result is dictionary parsed from json. It's okay but now I want to parse dictionary to my class, check if exists in database, if it exists update properties and if not than add new category to database.
My approach that I was using in apps before was that I have two classes: Category and CategoryCD (CD like Core Data) and first I parse json to Category class than for all categories I check if any CategoryCD (saved in CD) has same Id and then update, or add or other things.
Now I am thinking if there is better way how can I do it. I could each time when I download new results delete my database for this class and then add all results. The problem with this way is that what if I have something for some classes that I want keep. Like if I download and save images then I would rather still have connection between class and saved image.
I was thinking about my old approach but reduce 2 almost same classes (1 for Core Data and 1 same but for parsing) to 1 Core Data class. But then there is a problem when I init this class I must always create in database right? So It could be complicated.
What are you using for this? I think it's very common problem. You have list of items and I would like to have them available offline (with all data I downloaded before) but than when I connect to internet I would like to update with new results (not download all from server, just responses which I requested).
It is a very common problem and it has been solved hundreds of ways. BTW, This is not "synchronizing" it is "caching" for offline use.
Do not create two objects as that is redundant. Create just the Core Data objects as part of the parse.
A standard JSON parse would look like this:
Convert to Objects using NSJSONSerializer.
Fetch all unique IDs from the JSON objects using KVO
Fetch all existing objects from Core Data based on uniqueIDs
Insert all objects that do not currently exist
If you are going to update objects then #4 would do both.
Search on Stackoverflow and you will find plenty of examples of how to do this.
I do something similar in one of my apps.
Here is some generic code to fetch an item and update it. If it doesn't exist, it creates it.
func insertOrUpdateManagedObject(id: Int16, name: String) -> YourManagedObject? {
var managedObject: YourManagedObject?
if let context = self.managedObjectContext {
if let fetchResult = fetchManagedObjectWithId(id) {
managedObject = fetchResult
managedObject?.name = name
}
else {
managedPhrase = NSEntityDescription.insertNewObjectForEntityForName("YourManagedObject", inManagedObjectContext: context) as? YourManagedObject
managedObject?.id = id
managedObject?.name = name
}
}
println("Created a managed object \(managedObject)")
return managedObject
}