Unexpected behaviour - unable to access new elements in an array driven by an NSFetchedResultController (SwiftUI) - mvvm

I have a SwiftUI app that uses the MVVM design pattern in places where the underlying logic driving the View is either verbose or unit testing is advisable. In certain places I have taken to using a NSFetchedResultsController in conjunction with #Published properties and, early in development, this behaved as I would expect.
However, I have now encountered a situation where an addition to the CoreData store triggers controllerDidChangeContent and the array populated by controller.fetchedObjects has an appropriate number of elements but, for reasons I cannot fathom, I am unable to access the newest elements.
There is a certain amount of data processing which, as I'm working with an array by this point, I didn't think would cause a problem. I'm more suspicious that relationships may be responsible in some way and/or faulting is responsible (although adjusting faulting behaviour on the underlying fetch request failed to resolve the issue).
Interestingly, some similar code elsewhere in the app that uses #FetchRequest (because the View is simpler and so a ViewModel wasn't considered necessary) doesn't seem to suffer from the same problem.
Normally scattering debugging around has put me back on track but not today! I've included the console output - as you can see, as new entries (timestamped) are added, the total observation count increases but the most property which should reflect the most recent observation does not change. Any pointers would be gratefully received as always.
I can't really prune the code on this without losing context - apologies in advance for the verbosity ;-)
ViewModel:
extension ParameterGridView {
final class ViewModel: NSObject, ObservableObject, NSFetchedResultsControllerDelegate {
#Published var parameters: [Parameter] = []
#Published var lastObservation: [Parameter : Double] = [:]
#Published var recentObservation: [Parameter : Double] = [:]
let patient: Patient
private let dataController: DataController
private let viewContext: NSManagedObjectContext
private let frc: NSFetchedResultsController<Observation>
var observations: [Observation] = []
init(patient: Patient, dataController: DataController) {
self.patient = patient
self.dataController = dataController
self.viewContext = dataController.container.viewContext
let parameterFetch = Parameter.fetchAll
self.parameters = try! dataController.container.viewContext.fetch(parameterFetch)
let observationFetch = Observation.fetchAllDateSorted(for: patient)
self.frc = NSFetchedResultsController(
fetchRequest: observationFetch,
managedObjectContext: dataController.container.viewContext,
sectionNameKeyPath: nil,
cacheName: nil)
try! self.frc.performFetch()
observations = self.frc.fetchedObjects ?? []
super.init()
frc.delegate = self
updateHistoricalObservations()
}
// MARK: - METHODS
/// UI controls for entering new Observations default to the last value entered
/// This function calculates the median value for the Parameter's reference range to be used in the event no historical observations are available
/// - Parameter parameter: Parameter used to derive start value
/// - Returns: median value for the Parameter's reference range
func medianReferenceRangeFor(_ parameter: Parameter) -> Double {
let rangeMagnitude = parameter.referenceRange.upperBound - parameter.referenceRange.lowerBound
return parameter.referenceRange.lowerBound + (rangeMagnitude / 2)
}
/// Adds a new Observation to the Core Data store
/// - Parameters:
/// - parameter: Parameter for the observation
/// - value: Observation value
func addObservationFor(_ parameter: Parameter, with value: Double) {
_ = Observation.create(in: viewContext,
patient: patient,
parameter: parameter,
numericValue: value)
try! viewContext.save()
}
/// Obtains clinically relevant historical observations from the dataset for each Parameter
/// lastObservation = an observation within the last 15 minutes
/// recentObservation= an observation obtained within the last 4 hours
/// There may be better names for these!
private func updateHistoricalObservations() {
let lastObservationTimeLimit = Date.now.offset(.minute, value: -15)!.offset(.second, value: -1)!
let recentObservationTimeLimit = Date.now.offset(.hour, value: -4)!.offset(.second, value: -1)!
Logger.coreData.debug("New Observations.count = \(self.observations.count)")
let sortedObs = observations.sorted(by: { $0.timestamp < $1.timestamp })
let newestObs = sortedObs.first!
let oldestObs = sortedObs.last!
Logger.coreData.debug("Newest obs: \(newestObs.timestamp) || \(newestObs.numericValue)")
Logger.coreData.debug("Oldest obs: \(oldestObs.timestamp) || \(oldestObs.numericValue)")
for parameter in parameters {
var twoMostRecentObservatonsForParameter = observations
.filter { $0.cd_Parameter == parameter }
.prefix(2)
if let last = twoMostRecentObservatonsForParameter
.first(where: { $0.timestamp > lastObservationTimeLimit }) {
lastObservation[parameter] = last.numericValue
twoMostRecentObservatonsForParameter.removeAll(where: { $0.objectID == last.objectID })
} else {
lastObservation[parameter] = nil
}
recentObservation[parameter] = twoMostRecentObservatonsForParameter
.first(where: { $0.timestamp > recentObservationTimeLimit })?.numericValue
}
}
// MARK: - NSFetchedResultsControllerDelegate conformance
internal func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
let newObservations = controller.fetchedObjects as? [Observation] ?? []
observations = newObservations
updateHistoricalObservations()
}
}
}
NSManagedObject subclass:
extension Observation {
// Computed properties excluded to aid clarity
class func create(in context: NSManagedObjectContext,
patient: Patient,
parameter: Parameter,
numericValue: Double? = nil,
stringValue: String? = nil) -> Observation {
precondition(!((numericValue != nil) && (stringValue != nil)), "No values sent to initialiser")
let observation = Observation(context: context)
observation.cd_Patient = patient
observation.timestamp = Date.now
observation.parameter = parameter
if let value = numericValue {
observation.numericValue = value
} else {
observation.stringValue = stringValue!
}
try! context.save()
return observation
}
static var fetchAll: NSFetchRequest<Observation> {
let request = Observation.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(keyPath: \Observation.cd_timestamp, ascending: true)]
return request
}
static func fetchAllDateSorted(for patient: Patient) -> NSFetchRequest<Observation> {
let request = fetchAll
request.sortDescriptors = [NSSortDescriptor(keyPath: \Observation.cd_timestamp, ascending: true)]
request.predicate = NSPredicate(format: "%K == %#", #keyPath(Observation.cd_Patient), patient)
return request
}
static func fetchDateSorted(for patient: Patient, and parameter: Parameter) -> NSFetchRequest<Observation> {
let patientPredicate = NSPredicate(format: "%K == %#", #keyPath(Observation.cd_Patient), patient)
let parameterPredicate = NSPredicate(format: "%K == %#", #keyPath(Observation.cd_Parameter), parameter)
let compoundPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [patientPredicate, parameterPredicate])
let request = fetchAll
request.predicate = compoundPredicate
return request
}
}
Console output: (note observation count increments but the most recent observation does not change)

There is something wrong with your timestamps and/or sorting, the oldest observation is 4 days newer than the newest one (and it is in the future!)
Joakim was on the money - the timestamps are indeed incorrect; the problem was not in the logic but an error in the code (maths error relating to the TimeInterval between datapoints) that generated data for testing purposes. Garbage in, garbage out...
A lesson to me to be more careful - precondition now added to the function that generated the time series data (and a unit test!).
static func placeholderTimeSeries(for parameter: Parameter, startDate: Date, numberOfValues: Int) -> [(Date, Double)] {
let observationTimeInterval: TimeInterval = (60*5) // 5 minute intervals, not 5 hours! Test next time!!
let observationPeriodDuration: TimeInterval = observationTimeInterval * Double(numberOfValues)
let observationEndDate = startDate.advanced(by: observationPeriodDuration)
precondition(observationEndDate < Date.now, "Observation period end date is in the future")
return placeholderTimeSeries(valueRange: parameter.referenceRange,
valueDelta: parameter.controlStep...(3 * parameter.controlStep),
numberOfValues: numberOfValues,
startDate: startDate,
dataTimeInterval: observationTimeInterval)
}

Related

Swift - FetchRequest and filter results to display in list from CoreData

I am trying to create a fetch request that pulls data from CoreData, groups it by a id and then puts it into a list. I also want this to be dynamic, new list items can be added at any time while in this view. I have grinder my gears on this for hours and cannot figure out how to make this work dynamically. With my most recent attempt I am just getting initializing errors which I commented into the code. I am so stuck on this and would really appreciate some help. Also I am a total noob at swift so I am sorry if this is illiterate, Thanks
import CoreData
import SwiftUI
struct FilteredList: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest var fetchRequest: FetchedResults<Workout>
var filter: String
var collectedWorkout: [Int64 : [Workout]] = [:]
var uniqueWorkout: [Int64] = []
init(filterIn: String) {
filter = filterIn
//_fetchRequest = FetchRequest<Workout>(sortDescriptors: [SortDescriptor(\.order, order: .reverse)], predicate: NSPredicate(format: "type = %#", filterIn))
// _outerRequest = FetchRequest<Workout>(sortDescriptors: [SortDescriptor(\.order, order: .reverse)], predicate: NSPredicate(format: "new"))
let FR: NSFetchRequest<Workout> = Workout.fetchRequest()
let predicate = NSPredicate(format: "type = %#", filterIn)
var result: [Workout] = []
FR.sortDescriptors = [NSSortDescriptor(keyPath: \Workout.order, ascending: false)]
FR.predicate = predicate
do {
let wOuts: [Workout]
wOuts = try moc.fetch(FR) // <- Varialbe 'self.fetchRequest' used before being initialized
for wOut in wOuts {
print(wOut.last)
result = wOuts
}
} catch{
print("Unable to fetch")
result = []
}
// Then you can use your properties.
let unsortDict = Dictionary(grouping: result, by: { $0.workoutId })
uniqueWorkout = unsortDict.map({ $0.key }).sorted()
} // <- Return from initializer without initializing all stored properties

How to handle multiple queries of different HKQuantityTypeIdentifier

I'm trying to get multiple types of health data all with the same timeframe. My problem relies on the way that I should handle all the data that I get from those queries. Currently my model is as follows:
struct DailyData {
var steps: Int?
var distance: Int?
var calories: Int?
var exercise: Int?
}
class UserData {
let healthManager = HealthKitManager.shared
var dailyData = [DailyData]?
.
.
.
}
if I'm not mistaken, I can only query only one HKQuantityIdentifier at a time, so that means I need to call my getData() function from my HealthKitManager.swift once for every HKQuantityType that I have in my model:
func getData(type: HKQuantityTypeIdentifier, unit: HKUnit, days: Int, completed: #escaping (Result<[Int], Error>) -> Void) {
let calendar = NSCalendar.current
let interval = NSDateComponents()
interval.day = 1
let quantityType = HKQuantityType.quantityType(forIdentifier: type)!
var anchorComponents = calendar.dateComponents([.day, .month, .year], from: NSDate() as Date)
anchorComponents.hour = 0
let anchorDate = calendar.date(from: anchorComponents)
// Define 1-day intervals starting from 0:00
let query = HKStatisticsCollectionQuery(quantityType: quantityType,
quantitySamplePredicate: nil,
options: .cumulativeSum,
anchorDate: anchorDate!,
intervalComponents: interval as DateComponents)
query.initialResultsHandler = {query, results, error in
if let error = error {
completed(.failure(error))
return
}
let endDate = NSDate()
let startDate = calendar.date(byAdding: .day, value: -(days - 1), to: endDate as Date, wrappingComponents: false)
var completeDataArray: [Int] = []
if let myResults = results{
myResults.enumerateStatistics(from: startDate!, to: endDate as Date) { statistics, stop in
if let quantity = statistics.sumQuantity(){
let dayData = quantity.doubleValue(for: unit)
completeDataArray.append(Int(dayData))
}
}
}
completed(.success(completeDataArray))
}
healthStore.execute(query)
}
My problem is that I can't find a way to correctly set the received data into my model. Could someone point me in the right direction? I believe my model could be wrong, because as for what I have gather online, it's impossible to query multiple HKQuantityTypes in one query. Meaning that I would definitely have to set my model one [Int] of a HKtype at a time.
But with what I currently have, that would mean that when the first query returns I have to create multiple DailyData objects with almost all of the variables nil except the one I'm setting. Then, when the other queries return, I should do some array checking of dailyData matching the .count values with the one I got from the query, and that just feels wrong in my opinion. Not very elegant.
I tried another approach. Instead of having an array of a custom type, having a custom type that inside has an array of ints for every HKType that I need. But this has another problem: How could I "Keep in sync" the data from every HKType? having different arrays for every type would be, in my opinion, difficult to handle for example in a tableView. How could I set number of rows in section? Which type should I prefer? Also I would be accessing data over array indexes which may lead to bugs difficult to solve.
struct WeekData {
var steps: [Int]?
var distance: [Int]?
var calories: [Int]?
var exercise: [Int]?
}
class UserData {
let healthManager = HealthKitManager.shared
var weekData = WeekData()
.
.
.
}

How to use NSSet created from Core Data

I have the following core data model:
where Person to Codes is a one-to-many relationship.
I have a function which returns a Person record and if the code person.codes returns an NSSet of all the codes associated with that Person. The issue that I am having is how to use the NSSet.
person.codes.allObjects.first returns this data:
<Codes: 0x60000213cb40> (entity: Codes; id: 0xb978dbf34ddb849 <x-coredata://A2B634E4-E136-48E1-B2C5-82B6B68FBE44/Codes/p1> ; data: {
code = 4LQ;
number = 1;
whosAccount = "0xb978dbf34ddb869 <x-coredata://A2B634E4-E136-48E1-B2C5-82B6B68FBE44/Person/p1>";
})
I thought if I made person.codes.allObjects.first of type Codes, I would be able to access the code and number elements but I get an error: error: value of type 'Any?' has no member 'number'
Also, how can I search this data set for a particular code or number.
I appreciate that this is proabably a simple question but have searched and read the documentation to no avail. I suspect that may base knowledge is not sufficient.
Update
I have a CoreDataHandler class which contains the following code:
class CoreDataHandler: NSObject {
//static let sharedInstance = CoreDataHandler()
private static func getContext() -> NSManagedObjectContext {
let appDelegate = NSApplication.shared.delegate as! AppDelegate
return appDelegate.persistentContainer.viewContext
}
static func fetchPerson() -> [Person]? {
let context = getContext()
do {
let persons: [Person] = try context.fetch(Person.fetchRequest())
return persons
} catch {
return nil
}
}
I can fetch a person using:
let row = personTableView.selectedRow
let person = CoreDataHandler.fetchPerson()?[row]
Core Data supports widely native Swift types.
Declare codes as Set<Codes> in the Person class.
It's much more convenient than typeless NSSet.
You get a strong type and you can apply all native functions like filter, sort, etc. without type cast.
let codes = person.codes as! Set<Code>
Once that is done you can access the properties. Searching can be done by filtering for instance
let filteredCodes = codes.filter({ $0.code == "XYZ" })
will return all objects that has the code "XYZ". Or to get only one you can use
let code = codes.first(where: {$0.id == 1})
which will return the first object that has id = 1
A simple example getting all Person objects that has a given code
func findWithCode(_ code: String) -> [Person] {
guard let persons = CoreDataHandler.fetchPerson() else {
return []
}
var result = [Person]()
for person in persons {
let codes = person.codes as! Set<Code>
if codes.contains(where: { $0.code == code }) {
result.append(person)
}
}
return persons
}

Getting invalid property name when trying to perform Realm migration

I'm having trouble with Realm giving me the error that a property of a given name does not exist for my object. But I know it does exist.
I've tried to follow the docs at https://realm.io/docs/swift/latest/#updating-values. I've searched for everything I can think of to find an applicable solution here and elsewhere, but nothing I've found works.
I previously performed a simple migration to just add properties to a different object within the same Realm. I just left the migration block empty and that worked fine. That migration should have set my schema from 0 to 1. If I run this with my schema set to 1 and to check for versions less than 2, it tells me that migration must be run in order to add these properties. The migration is running. I have a print statement for every time it executes. If I set the schema to 2, still checking for less than 2, I get the error for invalid property name for the old properties unless I uncomment them. Then I still get the error for the new properties.
Here's my Realm object. The commented out lines are the old properties I want to migrate from. The Int values are what I'm migrating to.
#objcMembers class Options: Object {
// dynamic var morningStartTime: Date?
// dynamic var afternoonStartTime: Date?
// dynamic var eveningStartTime: Date?
// dynamic var nightStartTime: Date?
dynamic var morningHour: Int = 7
dynamic var morningMinute: Int = 0
dynamic var afternoonHour: Int = 12
dynamic var afternoonMinute: Int = 0
dynamic var eveningHour: Int = 17
dynamic var eveningMinute: Int = 0
dynamic var nightHour: Int = 21
dynamic var nightMinute: Int = 0
dynamic var morningNotificationsOn: Bool = true
dynamic var afternoonNotificationsOn: Bool = true
dynamic var eveningNotificationsOn: Bool = true
dynamic var nightNotificationsOn: Bool = true
dynamic var firstItemAdded: Bool = false
dynamic var smartSnooze: Bool = false
dynamic var optionsKey = UUID().uuidString
override static func primaryKey() -> String? {
return "optionsKey"
}
}
My migration block:
let config = Realm.Configuration(
// Set the new schema version. This must be greater than the previously used
// version (if you've never set a schema version before, the version is 0).
schemaVersion: 2,
// Set the block which will be called automatically when opening a Realm with
// a schema version lower than the one set above
migrationBlock: { migration, oldSchemaVersion in
// We haven’t migrated anything yet, so oldSchemaVersion == 0
if (oldSchemaVersion < 2) {
migration.enumerateObjects(ofType: Options.className(), { (newObject, oldObject) in
let morningStartTime = oldObject!["morningStartTime"] as! Date?
let afternoonStartTime = oldObject!["afternoonStartTime"] as! Date?
let eveningStartTime = oldObject!["eveningStartTime"] as! Date?
let nightStartTime = oldObject!["nightStartTime"] as! Date?
newObject!["morningHour"] = self.getHour(date: morningStartTime)
newObject!["morningMinute"] = self.getMinute(date: morningStartTime)
newObject!["afternoonHour"] = self.getHour(date: afternoonStartTime)
newObject!["afternoonMinute"] = self.getMinute(date: afternoonStartTime)
newObject!["eveningHour"] = self.getHour(date: eveningStartTime)
newObject!["eveningMinute"] = self.getMinute(date: eveningStartTime)
newObject!["nightHour"] = self.getHour(date: nightStartTime)
newObject!["nightMinute"] = self.getMinute(date: nightStartTime)
})
}
})
getHour and getMinute are just functions I wrote to return an Int for the hour or minute from a Date. In case it's relevant, here they are.
func getHour(date: Date?) -> Int {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HH"
let hour = dateFormatter.string(from: date!)
return Int(hour)!
}
func getMinute(date: Date?) -> Int {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "mm"
let minutes = dateFormatter.string(from: date!)
return Int(minutes)!
}
I know this is not the way to do it, but I made it work by taking a slightly more manual approach to the migration block. I uncommented the old properties in the Options object and changed my migration function to the following:
func migrateRealm() {
let configCheck = Realm.Configuration();
do {
let fileUrlIs = try schemaVersionAtURL(configCheck.fileURL!)
print("schema version \(fileUrlIs)")
} catch {
print(error)
}
print("performing realm migration")
let config = Realm.Configuration(
// Set the new schema version. This must be greater than the previously used
// version (if you've never set a schema version before, the version is 0).
schemaVersion: 2,
// Set the block which will be called automatically when opening a Realm with
// a schema version lower than the one set above
migrationBlock: { migration, oldSchemaVersion in
print("oldSchemaVersion: \(oldSchemaVersion)")
if (oldSchemaVersion < 2) {
print("Migration block running")
DispatchQueue(label: self.realmDispatchQueueLabel).async {
autoreleasepool {
let realm = try! Realm()
let options = realm.object(ofType: Options.self, forPrimaryKey: self.optionsKey)
do {
try realm.write {
if let morningTime = options?.morningStartTime {
options?.morningHour = self.getHour(date: morningTime)
options?.morningMinute = self.getMinute(date: morningTime)
}
if let afternoonTime = options?.afternoonStartTime {
options?.afternoonHour = self.getHour(date: afternoonTime)
options?.afternoonMinute = self.getMinute(date: afternoonTime)
}
if let eveningTime = options?.eveningStartTime {
options?.eveningHour = self.getHour(date: eveningTime)
options?.eveningMinute = self.getMinute(date: eveningTime)
}
if let nightTime = options?.nightStartTime {
options?.nightHour = self.getHour(date: nightTime)
options?.nightMinute = self.getMinute(date: nightTime)
}
}
} catch {
print("Error with migration")
}
}
}
}
})
// Tell Realm to use this new configuration object for the default Realm
Realm.Configuration.defaultConfiguration = config
// Now that we've told Realm how to handle the schema change, opening the file
// will automatically perform the migration
_ = try! Realm()
}
This only worked if I queued it on another thread asynchronously. I imagine if this data had been necessary for my initial view controller, then it probably would have created a race condition that caused the app to crash. Luckily this only appears in a secondary view, so it had time to complete before these values are needed. I guess I'll have to remove the unused properties with an updated Realm schema in a future version of my app.

databaseReference.observe(DataEventType.value, with:{(DataSnapshot) not working properly all the time

func checkPaid(utilityId : String) -> Int{
var amount:String = ""
var status = 0
print("inside new function ")
print ("\(utilityId) inside new function ")
self.databaseRefPayment.observe(DataEventType.value, with:{(DataSnapshot) in
if DataSnapshot.childrenCount > 0 {
for payments in DataSnapshot.children.allObjects as! [DataSnapshot]{
var paymentsObject = payments.value as? NSDictionary
/*
if(paymentsObject!["month"] as! String == monthCheck && paymentsObject!["year"] as! String == monthCheck && paymentsObject!["utilityid"] as! String == utilityId as! String){ */
if(paymentsObject!["utilityId"] as! String == utilityId){
amount = paymentsObject!["amount"] as! String
print(amount)
print("Ypur program is working perfect")
status = 1
}
}
}
})
return status
}
The above function is filtering the data present in payments node based on the value for utilityId getting passed in the function . But the strange thing is observe(DataEventType.value, with:{(DataSnapshot) this event is not getting triggered all the time . Its just skipping that portion unnecessarily . I am very new to firebase and getting really mad with these kind of unpredicted behaviours . Please help me in this . feel free to ask for any clarifications .
The firebase executes firebase query functions in different thread , so after u call check paid(), it runs the checkpaid() firebase query in another thread,and it will return from the function , eventhough ur query is running in the background..so it will seem like,checkpaid() is not working , but actually it's running on another thread.
I think you first fetch all the required data from payment, and store it in a list , and then use that list to compare with utility.
Every time this function is called it adds/resets the Key-Value Observer for whichever child node you are observing it doesn't actually check the value unless it is changed. I believe it is your intention to call checkPaid(utilityId:) to check the child is 'paid' by some means. There is no need to add a KVO if you are directly reading the value for a single snapshot. consider the following:
func checkPaid(utilityId: String) -> Bool {
//Assume it is not paid if we cannot verify it.
var isPaid = false
//Create a new reference to Firebase Database
var ref: DatabaseReference!
ref = Database.database().reference().child(utilityId)
//Get the values for the child, test if it is paid or not.
ref.queryOrderedByValue().observeSingleEvent(of: .value) { (snapshot) in
if (snapshot.value is NSNull) {
print("No Child With \(utilityId) Exists")
} else {
//child with utilityId exists, in case multiple utilityId's exist with the same value..
for child in snapshot.children.allObjects as! [DataSnapshot] {
if let values = child.value as? [String : AnyObject] {
let uid = child.key //utilityId
var month:String = ""
var year:String = ""
var amount:String = ""
//var amount:Double = 0.0
//get values from parent
if let m = values["month"] as? String {
month = m
}
if let y = values["year"] as? String {
year = y
}
if let a = values["amount"] as? String {
amount = a
}
/*
if let a = values["amount"] as? Double {
amount = a
}
*/
//??
if ((month == monthCheck) && (year == monthCheck)) {
isPaid = true
}
}
}
}
return isPaid
}
I am making one assumption here; that utilityId is the key for the child.
if you have parent nodes to utilityId you'll have to transverse those as well when you reference the database:
ref = Database.database().reference().child(utilities).child(utilityId) ..etc
If you need a KVO to update a local property I suggest adding/calling it in viewDidLoad, it's completion handler should take care of updating whichever properties are updated when they change in Firebase.