CoreData Aggregate one to many relationships - swift

I have a CoreData model with the following "architecture": I have a Session entity with system_version as an attribute. Then I have a Views (Session<-->>Views) entity with a delta_time an attribute. Finally I have a "UI_Elements" (Views <-->> UI_Elements) entity with a variable_value attribute. I would like to filter the content according to a specific variable_value value and then display the delta_time average grouped by system_version.
I have tried the following implementation:
var expressionDescriptions = [AnyObject]()
expressionDescriptions.append("views_ui.sessions.systemVersion" as AnyObject)
let expressionDescription = NSExpressionDescription()
expressionDescription.name = "Average Time"
expressionDescription.expression = NSExpression(format: "#avg.views_ui.delta_time_number")
expressionDescription.expressionResultType = .integer32AttributeType
expressionDescriptions.append(expressionDescription)
let fetch = NSFetchRequest<NSFetchRequestResult>(entityName: "UI_Elements")
fetch.predicate = requete
fetch.propertiesToGroupBy = ["views_ui.sessions.systemVersion"]
fetch.resultType = .dictionaryResultType
fetch.sortDescriptors = [NSSortDescriptor(key: "views_ui.sessions.systemVersion", ascending: true, selector: #selector(NSString.localizedStandardCompare(_:)))]
fetch.propertiesToFetch = expressionDescriptions
var results:[[String:AnyObject]]?
Unfortunately I got the following error:
CoreData: error: SQLCore dispatchRequest: exception handling request:
, Invalid keypath (request
for aggregate operation on a toOne-only keypath):
views_ui.delta_time_number with userInfo of (null)

I finally managed to get the desired result by modifying the code as follows:
let keypathExp = NSExpression(forKeyPath: "views_ui.delta_time_number")
expressionDescription.expression = NSExpression(forFunction: "average:", arguments: [keypathExp])

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

CoreData Fetch: Getting the objects with the maximum of one atttribute and having another attribute in common

I have a CoreData ManagedObject type Event with the properties name:Sting and date:Date.
I want to fetch all EventObject, with the name containing a filter. If more than one object with the same name matches the filter, only the object with the latest date should be returned.
Just to clarify what I want. In a table based approach I would query in SQL like:
SELECT name, max(date) FROM Event
WHERE name contains 'filter'
GROUP BY name
In CoreData I tried like this, but got:
Could not cast value of type 'NSKnownKeysDictionary1' to 'xx12.Event'.
let fetchRequest : NSFetchRequest<Event> = Event.fetchRequest( fetchRequest.predicate = NSPredicate (format: "name contains %#", filter)
fetchRequest.sortDescriptors = [NSSortDescriptor (key: "name", ascending: true)]
// We want Event not Dictionary back
// fetchRequest.resultType = NSFetchRequestResultType.dictionaryResultType
let keypathExpression = NSExpression(forKeyPath: "date")
let maxExpression = NSExpression(forFunction: "max:", arguments: [keypathExpression])
let key = "maxDate"
let expressionDescription = NSExpressionDescription()
expressionDescription.name = key
expressionDescription.expression = maxExpression
expressionDescription.expressionResultType = .dateAttributeType
fetchRequest.propertiesToFetch = [expressionDescription]
let resultEvents = try moc.fetch(fetchRequest)
Example:
if I have these objects
meeting1, 1.1.2020
meeting1, 1.4.2020
meeting2, 1.1.2020
meeting2, 1.4.2020
party, 2.3.2020
and the filter: meeting
I want to get the objects:
meeting1, 1.4.2020
meeting2, 1.4.2020
I would use a basic fetch request and then do the grouping part in swift code instead of working with NSExpression
let fetchRequest : NSFetchRequest<Event> = Event.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "name contains %#", filter)
let result = try moc.fetch(fetchRequest)
return Dictionary(grouping: result, by: \.name).values
.compactMap { $0.max {$0.date < $1.date } }

How to add "one-to" part of "one-to-many" to fetch results

I want to be able to add the player's data the "to-one" part of the many relationships. The fetch does some aggregation for me, but I would like to know what player it belongs to.
I have a CoreData model that looks like the following:
I have a fetch request that looks like the following:
func statsPerPlayer(player: Players, managedContext: NSManagedObjectContext) -> [String: Int] {
var resultsDic = [String: Int]()
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Shifts")
let predicate = NSPredicate(format: "playersRelationship = %#", player)
fetchRequest.predicate = predicate
let nsExpressionForKeyPath = NSExpression(forKeyPath: "timeOnIce")
let nsExpressionForFunctionMin = NSExpression(forFunction: "min:", arguments: [nsExpressionForKeyPath])
let nsExpressionDescriptionMin = NSExpressionDescription()
nsExpressionDescriptionMin.expression = nsExpressionForFunctionMin
nsExpressionDescriptionMin.name = "minShift"
nsExpressionDescriptionMin.expressionResultType = .integer16AttributeType
let nsExpressionForFunctionMax = NSExpression(forFunction: "max:", arguments: [nsExpressionForKeyPath])
let nsExpressionDescriptionMax = NSExpressionDescription()
nsExpressionDescriptionMax.expression = nsExpressionForFunctionMax
nsExpressionDescriptionMax.name = "maxShift"
nsExpressionDescriptionMax.expressionResultType = .integer16AttributeType
let nsExpressionForFunctionSum = NSExpression(forFunction: "sum:", arguments: [nsExpressionForKeyPath])
let nsExpressionDescriptionSum = NSExpressionDescription()
nsExpressionDescriptionSum.expression = nsExpressionForFunctionSum
nsExpressionDescriptionSum.name = "sumShift"
nsExpressionDescriptionSum.expressionResultType = .integer16AttributeType
let nsExpressionForFunctionAvg = NSExpression(forFunction: "average:", arguments: [nsExpressionForKeyPath])
let nsExpressionDescriptionAvg = NSExpressionDescription()
nsExpressionDescriptionAvg.expression = nsExpressionForFunctionAvg
nsExpressionDescriptionAvg.name = "avgShift"
nsExpressionDescriptionAvg.expressionResultType = .integer16AttributeType
let nsExpressionForFunctionCount = NSExpression(forFunction: "count:", arguments: [nsExpressionForKeyPath])
let nsExpressionDescriptionCount = NSExpressionDescription()
nsExpressionDescriptionCount.expression = nsExpressionForFunctionCount
nsExpressionDescriptionCount.name = "countShift"
nsExpressionDescriptionCount.expressionResultType = .integer16AttributeType
fetchRequest.propertiesToFetch = [nsExpressionDescriptionMin, nsExpressionDescriptionMax, nsExpressionDescriptionSum, nsExpressionDescriptionAvg, nsExpressionDescriptionCount]
fetchRequest.resultType = .dictionaryResultType
do {
let fetchArray = try managedContext.fetch(fetchRequest)
print(fetchArray)
resultsDic = fetchArray.first as! [String : Int]
} catch let error as NSError {
print("\(self) -> \(#function): Could not fetch. \(error), \(error.userInfo)")
}
return resultsDic
} //statsPerPlayer
The results look great and something like this:
[{
avgShift = 39;
countShift = 4;
maxShift = 89;
minShift = 6;
sumShift = 157;
}]
However, I would like to include the player that this data is for. How do I add the "to-one" part of the one-to-many relationship in the results?
Thanks!!
In your code above, just add the relevant relationship name to the properties to fetch:
fetchRequest.propertiesToFetch = ["playersRelationship", nsExpressionDescriptionMin, nsExpressionDescriptionMax, nsExpressionDescriptionSum, nsExpressionDescriptionAvg, nsExpressionDescriptionCount]
The dictionaries that are returned will then include the key "playersRelationship" with value set to the NSManagedObjectID of the corresponding Players object. You can then use the context's object(with:) method to access the Players object itself.
Update
So after some testing, it turns out:
a) CoreData gets confused regarding the count aggregate function if you include the relationship in the propertiesToFetch. That leads to the Invalid keypath (request for aggregate operation on a toOne-only keypath error.
b) CoreData gets confused for all the other aggregate functions if you include the relationship in the propertiesToFetch. (It calculates the aggregate across every object, not just those matching the predicate.)
The solution to both problems is to add the relationship as a GROUP BY property. CoreData then calculates the aggregates correctly and also correctly recognises count as a valid operation. So, add the following line:
fetchRequest.propertiesToGroupBy = ["playersRelationship"]

Filter navigation properties of one-to-many

I have a model Category with the relationship property articles with is an NSOrderedSet.
Now I want to get all Categories with the articles where a certain condition is fulfilled, in SQL I would write:
SELECT *
FROM Category AS cat
JOIN Article AS art ON art.categoryId = cat.categoryId AND art.gender='m';
I tried with:
NSPredicate(format: "articles.gender like %# OR articles.gender = %#", gender.lowercased(), "n")
I get the following error:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'to-many key not allowed here'
Complete code:
let ctx = self.Context()
var gender = UserDefaults.standard.string(forKey: "gender_preference") ?? "*"
if gender.uppercased() == "N" { gender = "*" }
// Create Fetch Request
let fetchRequest: NSFetchRequest = ArticleCategory.fetchRequest()
let predicate = NSPredicate(format: "articles.gender like %# or articles.gender = %#", gender, "n")
fetchRequest.predicate = predicate
// sort by name
let sort = NSSortDescriptor(key: "name", ascending: true)
fetchRequest.sortDescriptors = [sort]
do {
let result = try ctx.fetch(fetchRequest)
return result
} catch {
print(error)
}
return []
Kind Regards
EDIT:
I find it's more reliable to use SUBQUERY rather than ANY, ALL, NONE or SOME, particularly with compound clauses. Fetching Categories where ANY of its articles meet a condition is equivalent to fetching if the count of articles meeting the condition is greater than zero:
let predicate = NSPredicate(format: "SUBQUERY(articles, $a, $a.gender like %# OR $a.gender == %#).#count > 0", gender, "n")

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.