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!)
Related
I have a dashboard view that I'd like to display a few variables from my last entered CoreData entry. However, I can't figure out how to fetch only the last data entered into a variable so I can display it. Any ideas?
EDIT: I'm trying to setup a NSFetchRequest inside of a called function that is called only onappear. However, I'm getting errors and am lost.
func singleEntryPull() -> [Item] {
let request: NSFetchRequest<Item> = Item.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "todaysDate", ascending: false)]
request.fetchLimit = 1
let singleEntry = FetchRequest(fetchRequest: request)
return singleEntry
}
And then the return from the function should only show 1 result and I can then use the returned value to display the variables I need?
Well, not sure if this is the cleanest or best way to do this but I got it working like I want for now until a better solution comes up. I'm still using #FetchRequest which im now aware is pulling data live and updating it, but that might work as if someone keeps the app open overnight and updates it in the morning, I'd want it to display that latest entry. I used this:
ForEach(singleEntry.prefix(1)) { item in
A fetch limit of 1 on a fetch request will return you a single value. However, when you're setting up a #FetchRequest, you're doing more than this - you're making the initial fetch and then continuing to monitor the context for changes, so it live updates. This monitoring only uses the predicate of your fetch request.
Depending on your order of operations, you could be seeing the latest data, and then any new data inserted since you started that view. My experiments with the SwiftUI core data template project prove this out - on initial run you get the a single latest entry, but as you add newer ones, the fetch-limited screen picks up the new entries.
Depending on how this view is actually used, you have two choices - you can do an actual fetch request on appear of the view and store the result as an observable object, or you can make sure you only ever use the first record from the fetch request's results array, which will always be the latest record because of your sort ordering:
var body: some View {
if let latest = singleEntry.first {
// Some view describing the latest entry
} else {
Text("No record")
}
}
I need to observe changes of an Entity after import occurred.
Currently I have next logic:
Save Entity with temp identifier (NSManagedObject.objectId) to local core data storage.
Send Entity to the server via Alamofire POST request.
Server generates JSON and reply with the almost the same Entity details but with modified identifier which was NSManagedObject.objectId previously. So the local one Entity id will be updated with server id.
Now when I received new JSON I do transaction.importUniqueObjects.
At this step I want to inform my datasource about changes. And refetch data with updated identifiers.
So my DataSource has some Entities in an array, and while I use this datasource to show data it's still static information in that array which I fetched before, but as you see on the step number 4 I already updated core data storage via CoreStore import and want DataSource's array to be updated too.
I found some information regarding ListMonitor in CoreStore and tried to use it. As I can see this method works when update comes
func listMonitorDidChange(_ monitor: ListMonitor)
but I try to refetch data somehow. Looks like monitor already contains some most up to date info.
but when I do this:
func listMonitorDidChange(_ monitor: ListMonitor<MyEntity>) {
let entities = try? CoreStore.fetchAll(
From<MyEntity>()
.orderBy(.ascending(\.name))
) // THERE IS STILL old information in database, but monitor instance shows new info.
}
And then code became like this:
func listMonitorDidChange(_ monitor: ListMonitor<MyEntity>) {
var myEntitiesFromMonitor = [MyEntity]()
for index in 0...monitor.numberOfObjects() {
myEntitiesFromMonitor.append(monitor[index])
}
if myEntitiesFromMonitor.count > 0 {
// HERE we update DataSource
updateData(with: myEntitiesFromMonitor)
}
}
not sure if I am on the right way.
Please correct me if I am wrong:
As I understood each time core data gets updated with new changes, monitor gets updated as well. I have not dive deep into it how this was made, via some CoreData context notification or whatever but after you do something via CoreStore transaction, such as create or update or delete object or whatever you want, monitor gets update. Also it has callback functions that you need to implement in your class where you want to observe any changes with data model:
Your classes such as datasource or some service or even some view controller (if you don't use any MVVP or VIPER or other design patterns) need to conform to ListObserver protocol in case you want to listen not to just one object.
here are that functions:
func listMonitorDidChange(monitor: ListMonitor<MyPersonEntity>) {
// Here I reload my tableview and this monitor already has all needed info about sections and rows depend how you setup monitor.
// So you classVariableMonitor which I provide below already has up to date state after any changes with data.
}
func listMonitorDidRefetch(monitor: ListMonitor<MyPersonEntity>) {
// Not sure for which purposes it. I have not received this call yet
}
typealias ListEntityType = ExerciseEntity
let classVariableMonitor = CoreStore.monitorSectionedList(
From<ListEntityType>()
.sectionBy(#keyPath(ListEntityType.muscle.name)) { (sectionName) -> String? in
"\(String(describing: sectionName)) years old"
}
.orderBy(.ascending(\.name))
.where(
format: "%K == %#",
#keyPath(ListEntityType.name),
"Search string")
)
All other thing documented here so you can find info how to extract info from monitor in your tableview datasource function.
Thanks #MartinM for suggestion!
Suppose i have 100 data in DynamoDB then to remove all data i have to execute for loop .So is there any way to remove all data without for loop ?
let dynamoDbObjectMapper = AWSDynamoDBObjectMapper.default()
for var key in 0 ..<100 {
let itemToDelete: CategoriesDB = CategoriesDB()
itemToDelete._index = key
dynamoDbObjectMapper.remove(itemToDelete, completionHandler:
{(error: Error?) -> Void in
if let error = error {
print(" Amazon DynamoDB Save Error: \(error)")
return
}
print("A Category was deleted.")
})
}
If you want to remove all the data in a DynamoDB table, it is better to just delete the table and create it again as that wouldn't cost Write Capacity Units.
Ignoring the swift SDK for a moment, DynamoDB has the DeleteItem method to remove a single item from a table, but it also has a batch method.
Quote
In addition to DeleteItem, Amazon DynamoDB supports a BatchWriteItem
action for deleting multiple items at the same time.
The problem is, I can't see a way to access BatchWriteItem through the swift SDK. Even if you could use BatchWriteItem, it can only take 25 items at a time, so you would still end up having to write a loop.
Sorry its not a more complete answer, but thought this might be useful. As far as I know there isn't a simple (single) method for doing this.
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 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
}