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

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

Related

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

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

Swift diff realm.io without fetching it in advance

I was wondering if there is a possibility in realm.io (swift) to select all items from one "table" that are not in the other one.
Lets say you have 2 classes:
class A: Object {
dynamic var id: Int = 0
dynamic var text: String = ""
}
class B: Object {
dynamic var id: Int = 0
dynamic var value: Bool = false
}
Is it possible to get an result of items from A who's id is not present in B?
There is actually a very simple way to do this using NSPredicate on Realm filter API.
func fetch() throws -> [A] {
do {
// Create Realm
let realm = try Realm()
// Get B objects from Realm and put their IDs to [Int] array
let IdB: [Int] = realm.objects(B).map { $0.id }
// Create predicate
// Filter all items where property id is not present in array IdB
let predicateFilter = NSPredicate(format: "NOT (id IN %#)", IdB)
// Get all A objects from array using predicateFilter
let objectsA = realm.objects(A).filter(predicateFilter)
// Return the [A] array
return objectsA.map { $0 }
} catch {
// Throw an error if any
throw error
}
}
Also note that all objects from fetched using Realm are lazy loaded which means that this method is also very fast. From the documentation:
All queries (including queries and property access) are lazy in Realm. Data is only read when the properties are accessed.

How to read HKWorkoutActivityType.Running in healthkit?

I tried below code to read data from healthapp, But I'm getting results as nil value and also error Invalid HKObjectType HKQuantityTypeIdentifierDistanceWalkingRunning for keyPath workoutType.
mac version : 10.10.5
xcode version : 7.1
let distanceType =
HKObjectType.quantityTypeForIdentifier(
HKQuantityTypeIdentifierDistanceWalkingRunning)
let workoutPredicate = HKQuery.predicateForWorkoutsWithWorkoutActivityType(HKWorkoutActivityType.Running)
let startDateSort =
NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)
let query = HKSampleQuery(sampleType: distanceType!, predicate: workoutPredicate,
limit: 0, sortDescriptors: [startDateSort]) {
(sampleQuery, results, error) -> Void in
if let distanceSamples = results as? [HKQuantitySample] {
// process the detailed samples...
}
else {
// Perform proper error handling here...
print("*** An error occurred while adding a sample to " +
"the workout: \(error!.localizedDescription)")
abort()
}
}
// Execute the query
healthManager.healthKitStore.executeQuery(query)
below code to access healthkit data
// 1. Set the types you want to read from HK Store
let typeOfRead = [HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDietaryEnergyConsumed)!,
HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierActiveEnergyBurned)!,
HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeight)!,
HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierBodyMass)!,
HKObjectType.characteristicTypeForIdentifier(HKCharacteristicTypeIdentifierDateOfBirth)!,
HKObjectType.characteristicTypeForIdentifier(HKCharacteristicTypeIdentifierBiologicalSex)!,
HKObjectType.characteristicTypeForIdentifier(HKCharacteristicTypeIdentifierBloodType)!,
HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierStepCount)!,
HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDistanceWalkingRunning)!,
HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeartRate)!,
HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierOxygenSaturation)!,
HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierBloodPressureDiastolic)!,
HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierBloodPressureSystolic)!]
let typeOfReads = NSSet(array: typeOfRead)
// 2. Set the types you want to write to HK Store
let typeOfWrite = [
HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDietaryEnergyConsumed)!,
HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierActiveEnergyBurned)!,
HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeight)!,
HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierBodyMass)!,
HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierStepCount)!,
HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDistanceWalkingRunning)!
]
let typeOfWrites = NSSet(array: typeOfWrite)
use this:
let query = HKSampleQuery(sampleType: HKWorkoutType.workoutType(), predicate: workoutPredicate,
limit: 0, sortDescriptors: [startDateSort]) {
// ...
}
with the sampleType you say that you want to select workouts.
the predicate determines what workout properties are used to select.
You told the healthSore to select running samples with workout properties. That doesn't fit together.

Why Realm "to many" relationship having always the same reference?

Why is the realm-list containing the very same elements instead of different ones ?
As you can see in the picture below, there are two relam-objects (UndoMemoryNameEntry and NameEntry). The first one contains a list of 8 elements. The list's element-type is of type NameEntry !
My last NameEntry object is written with currentScorePlayer=1 and currentScoreMe=15 as you can see in the picture below:
The list in UndoMemoryNameEntry is correctly inserted the last NameEntry object. You find the insertion-code further down...
But now the problem: Why are all the existing list-elements as well changed to the newest inserted element ???? As you can see in the picture below, all the elements are unfortunately identical to the last one added - why ??????
If I change the NameEntry to the following :
And inserting at index=0 to the list, then the List changes to :
Why are all the elments changed ? And not just the inserted one ??? Thanks for any help on this !
My two realm-objects are :
class NameEntry: Object {
dynamic var playerName = ""
dynamic var isMyAdversary: Bool = false
dynamic var currentScorePlayer: Int = 0
dynamic var currentScoreMe: Int = 0
}
and the List :
class UndoMemoryNameEntry: Object {
dynamic var undoPlayerName = ""
let NameEntryList = List<NameEntry>()
}
The following code creates the Realm-List :
// query rlm for existing object (with name adversary
let undoPredicate = NSPredicate(format: "undoPlayerName == %#", adversaryName)
let undoPlayerName = rlm.objects(UndoMemoryNameEntry).sorted("undoPlayerName", ascending: true).filter(undoPredicate)
// if undoPlayerName object does not exist - then create it!
if (undoPlayerName.count < 1) {
rlm.beginWrite()
let undoEntry = UndoMemoryNameEntry()
undoEntry.undoPlayerName = adversaryName
rlm.add(undoEntry)
rlm.commitWrite()
}
The following code adds a "NameEntry"-Element in the List :
let undoPredicate = NSPredicate(format: "undoPlayerName == %#", plaNameLab)
let undoPlayerName = rlm.objects(UndoMemoryNameEntry).sorted("undoPlayerName", ascending: true).filter(undoPredicate)
if (undoPlayerName.count == 1) {
rlm.beginWrite()
println(entry)
var undoEntry = undoPlayerName[0] as UndoMemoryNameEntry
undoEntry.NameEntryList.insert(entry, atIndex: 0)
rlm.commitWrite()
}
The above code-excerts work perfectly - except that the realm-List always changes all its elements to the one just inserted.
I finally found a solution:
First of all rearrange the two realm objects as follows:
class NameEntry: Object {
dynamic var playerName = ""
dynamic var currentScorePlayer: Int = 0
dynamic var currentScoreMe: Int = 0
// the undo-list is better placed in the first object...
let undoEntryList = List<UndoMemoryNameEntry>()
override static func primaryKey() -> String? {
return "playerName"
}
}
class UndoMemoryNameEntry: Object {
dynamic var undoPlayerName = ""
dynamic var currentScorePlayer: Int = 0
dynamic var currentScoreMe: Int = 0
// no primary key here since the undoEntry will have several items with the same undoPlayerName
}
Then when adding a "NameEntry"-Element in the List :
let predicate = NSPredicate(format: "playerName == %#", plaNameLab)
let playerName = rlm.objects(NameEntry).sorted("playerName", ascending: true).filter(predicate)
if (playerName.count == 1) {
rlm.beginWrite()
var entry = playerName[0] as NameEntry
// you need to create a new list object first !!!!!!!!!!!!
// ...in my initial example, this creation was missing !!!!!!
var siblingEntry = UndoMemoryNameEntry()
siblingEntry.undoPlayerName = plaNameLab
siblingEntry.currentScorePlayer = entry.currentScorePlayer
siblingEntry.currentScoreMe = entry.currentScoreMe
// insert new list-element
entry.undoEntryList.insert(siblingEntry, atIndex: 0)
// alternatively choose append if you want to add the element at the end of the list
entry.undoEntryList.append(siblingEntry)
// or choose the "ringbuffer-solution" given in the add-on below if you want to restrict the number of list-elements to ringbuffer-size !
// ...
rlm.commitWrite()
}
Add-on: If you want to create a ringbuffer having only a limited number of list-elements:
// create ringbuffer of 20 elements (20th element will be newest)
let ringBufferSize = 20
let undoPredicate = NSPredicate(format: "undoPlayerName == %#", plaNameLab)
if (entry.undoEntryList.filter(undoPredicate).sorted("undoPlayerName").count < ringBufferSize) {
entry.undoEntryList.append(siblingEntry)
}
else {
// entry.undoEntryList.replace(ringBufferSize-1, object: siblingEntry)
entry.undoEntryList.removeAtIndex(ringBufferSize-1)
entry.undoEntryList.append(siblingEntry)
for index in 0..<ringBufferSize-1 {
let tempEntry1 = rlm.objects(UndoMemoryNameEntry).filter(undoPredicate).sorted("undoPlayerName")[index] as UndoMemoryNameEntry
let tempEntry2 = rlm.objects(UndoMemoryNameEntry).filter(undoPredicate).sorted("undoPlayerName")[index+1] as UndoMemoryNameEntry
tempEntry1.currentScorePlayer = tempEntry2.currentScorePlayer
tempEntry1.currentScoreMe = tempEntry2.currentScoreMe
}
let tempEntry = rlm.objects(UndoMemoryNameEntry).filter(undoPredicate).sorted("undoPlayerName")[ringBufferSize-1] as UndoMemoryNameEntry
rlm.delete(tempEntry)
}

Swift Core Data Quirk

Can anyone help me figure out why this code is behaving thusly...
When opening the app for the first time a "User" is created (if it doesn't already exist, which it doesn't the first time) and then the user is saved along with his/her golf "clubs". I get confirmation of the user saved and the clubs saved in the console. HOWEVER, when I close the app and reopen it the user is fetched but the clubs are not. What am I missing here? Let me know if you need/want to see any screen captures beyond this code...
//MARK: Core Data Variables
var user : User!
var userClubs = NSMutableSet()
var currentRound : Round!
var managedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext!
func prepareUser() {
let fetchRequest = NSFetchRequest(entityName: "User")
let sortDescriptor = NSSortDescriptor(key: "createdTime", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
if let fetchResults = self.managedObjectContext.executeFetchRequest(fetchRequest, error: nil) as? [User] {
if fetchResults.count > 0 {
self.user = fetchResults[0]
println("First user: \(self.user!.firstName) \(self.user!.lastName)")
let fetchRequestClubs = NSFetchRequest(entityName: "Club")
if let fetchResults2 = self.managedObjectContext.executeFetchRequest(fetchRequestClubs, error: nil) as? [Club] {
if fetchResults2.count > 0 {
println("test: \(fetchResults2[0].type)")
}
}
} else {
println("No user yet")
var newUser : User = NSEntityDescription.insertNewObjectForEntityForName("User", inManagedObjectContext: self.managedObjectContext) as! User
newUser.createdTime = NSDate()
managedObjectContext.save(nil)
var i = 0
println("before array: clubsArray is \(clubsArray.count) clubs long")
var clubs = NSMutableSet.mutableSetValueForKey("clubs")
for newClub in clubsArray {
var club : Club = NSEntityDescription.insertNewObjectForEntityForName("Club", inManagedObjectContext: self.managedObjectContext) as! Club
club.type = clubsArray[i].type as String
club.estimatedMinYardage = clubsArray[i].minDistance as Int
club.estimatedMaxYardage = clubsArray[i].maxDistance as Int
club.lowerBound = clubsArray[i].lowerBound as Int
club.upperBound = clubsArray[i].upperBound as Int
//userClubs.addObject(club)
managedObjectContext.save(nil)
//club.setValue(newUser, forKey: "user")
println("\(club.type)")
i++
}
//user.setValue(userClubs, forKey: "clubs")
prepareUser()
}
}
}
Here's the console output from the first run:
No user yet
before array: clubsArray is 17 clubs long
Putter
LW
SW
PW
9i
8i
7i
6i
5i
5W
... [the rest of the clubs]
First user: Your Name
test: 7i
And from the second run after closing and reopening the app:
First user: Your Name
Bloody hell. I was trying to set 1 of the 17 instances of the Club class to a value that was less than it's minimum value allowed in the data model. If anyone else is having a similar issue check your min/max and default in addition to what #Tom-Harrington suggested in the comments of the original question. Dunno if Apple could get some kind of warning for this kind of thing into future versions of XCode.