CoreStore how to observe changes in database - ios

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!

Related

Is there a way to pull the last CoreData Entry only?

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")
}
}

NSFetchedResultsController not seeing inserts made by extension

I have an iOS app structured like this
Main Application (the main iOS app)
Intents Extension (Siri integration)
Shared Framework (shared library for interacting with Core Data. This allows both the main application and the intents extension to use the same Core Data store)
My issue is that when I insert something into Core Data using the Intents Extension, it doesn't appear in the Main Application's UITableView until I manually refresh the fetchedResultsController like this:
NSFetchedResultsController<NSFetchRequestResult>.deleteCache(withName: "myCache")
try? fetchedResultsController.performFetch()
tableView.reloadData()
Is there a way to make the fetchedResultsController see the changes without having to manually refresh everything?
Note: If I insert something into core data from the Main Application, the fetchedResultsController automatically sees the change and updates the table (like expected)
To share a database between an app and extension you need to implement Persistent History Tracking. For an introduction see WWDC 2017 What's New in Core Data at 20:49 and for sample code see the documentation Consuming Relevant Store Changes.
The basic idea is to enable the store option NSPersistentHistoryTrackingKey, observe NSPersistentStoreRemoteChangeNotification, upon being notified you should fetch the changes using NSPersistentHistoryChangeRequest and then merge into the context using mergeChangesFromContextDidSaveNotification and transaction.objectIDNotification. Your NSFetchedResultsController will then update accordingly.
This is normal because the application extension and the main application are not working in the same process.
There are some ways to update the data in the main application
NSPersistentStoreRemoteChangeNotification
UserDefaults(suitename:)
Darwin Notifications
I'm using UserDefaults and refreshAllObjects function for the viewContext.
Example:
func sceneDidBecomeActive(_ scene: UIScene) {
let defaults = UserDefaults(suiteName:"your app group name")
let hasChange = defaults?.bool(forKey: "changes")
if hasChange ?? false {
refreshAllObjects()
defaults?.set(false, forKey: "changes")
}
}
refresh all objects function is like this:
viewContext.perform {
viewContext.stalenessInterval = 0.0
viewContext.refreshAllObjects()
viewContext.stalenessInterval = -1
}

One To Many Relationship setter

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!)

How to prevent using cache when new data is available?

I'm caching data in CoreData within my app to reduce updating request when there's nothing new.
Here's my caching logic in pseudo code:
if cacheExistsInCoreData {
if cacheIsOutdated {
loadDataFromRemoteAndCacheItWithCurrentDate()
}else {
useCache()
}else {
loadDataFromRemoteAndCacheItWithCurrentDate()
}
How I check if cache is outdated:
func checkIfCacheIsOutdated {
if lastCachedDate isOrderThan selfDefinedCheckingDate {
return true // need to load new data
}else {
return false // just use cache
}
}
This mechanism works fine almost all the time.
While in rare situation I find my program caches the wrong data with a right date, which means user might see the older data and could not get update when new one is available.
If there's nothing wrong with my caching logic, I wonder if the reason could be that when the remote data is fetched by my app before it gets updated and then gets stored in core data with the latest date time.
A cache in core data includes:
data(provided by remote server) //nothing I can do with it...
date(provided by me using NSDate())
How can I make sure if the two objects are correctly connected (latest data with current time) before storing them?
Assuming you are calling a web api, most of the web apis would return cache headers. It is safe to cache the data based on cache header value, that way there wont be any stale cache.
The solution that I came across that works for me is to set the stalenessInterval on the Main Queue NSManagedObjectContext as follows inside of your extension:
objectContext.stalenessInterval = 0.0;
This tells the context in the extension to fetch new data every time and ignore the cache.

iOS synchronize json list of objects from server

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
}

Resources