Conditional CKSubscription does not trigger notification - cloudkit

I have a OUP record type, and it has a .DeleteSelf reference to a OU record type.
If I just set up an unconditional subscription, that works:
let subscriptionOUP = CKSubscription(recordType: "OUP", predicate:NSPredicate(value: true), subscriptionID: "oup", options: .FiresOnRecordUpdate)
But the conditional way not:
let ekf = "oup-\(ou.recordID)"
let pr = NSPredicate(format: "ou = %#", CKReference(recordID: CKRecordID(recordName: organizationUser.recordID), action: .DeleteSelf))
let subscriptionOUP = CKSubscription(recordType: "OUP", predicate:pr, subscriptionID: ekf, options: .FiresOnRecordUpdate)
println("\(pr) \(ekf)")
Terminal content:
ou == <CKReference: 0x1558d3d0;51D56239-D68C-4E12-BD23-E4DCEF5721B7:(_defaultZone:__defaultOwner__)>
oup-51D56239-D68C-4E12-BD23-E4DCEF5721B7
What is the problem with my predicate or subscriptionID?

The subscriptionID string is too long.
When you reduce it like this, it will work:
let ekf = "oup-\((organizationUser.recordID as NSString).substringToIndex(7))"

Related

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 write an NSPredicate to search date attribute of to-many relationship?

I have a "Patient" object with a to-many relationship to an "Act" object (Patient.acts)
An Act object has a startDate attribute of type Date
I am trying to write the NSPredicate to get all patients who have at least one act whose startDate is today
I am on XCode 10, using swift 4.2
I cannot figure out the correct left expression in:
let start = NSExpression(forConstantValue: Calendar.current.startOfDay(for: Date()))
let end = NSExpression(forConstantValue: Calendar.current.endOfDay(for: Date()))
let todayRange = NSExpression(forAggregate: [start, end])
let actToday = NSComparisonPredicate(leftExpression: ????, rightExpression: todayRange, modifier: .any, type: .between, options: [])
for the leftExpression, I have tried several options but nothing works:
let left = NSExpression(forConstantValue: "acts.startDate")
I have tried a subpredicate approach:
let actDate = NSPredicate(format: "$x.startDate")
let left = NSExpression(forSubquery: NSExpression(forKeyPath: "acts"), usingIteratorVariable: "x", predicate: actDate2)
UPDATE and answer
I have come up with the following expression:
let seenToday = NSPredicate(format: "SUBQUERY(acts, $v, $v.actStartDate => %# && $v.actStartDate < %#).#count!=0", argumentArray:[startToday, endToday] )
Thanks to vadian for his answer. The question I wonder now is, is it more efficient to do it the way vadian proposes or is my subquery predicate adequate.
I'd recommend to fetch all acts for the given date and map them to their patient attribute (assuming there is an appropriate reverse relationship in Act)
let interval = Calendar.current.dateInterval(of: .day, for: Date())!
let fetchRequest : NSFetchRequest<Act> = Act.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "startDate BETWEEN %#", [interval.start, interval.end] as [CVarArg])
Then fetch the acts and map the result to a set of patients
let acts = try context.fetch(fetchRequest)
let patients = Set(acts.map{$0.patient})

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

SQL's IN clause equivalent in Swift

I wanted to know if there is any equivalent command in Core Data / Swift of MySQL's IN clause:
"SELECT details FROM tbl_details WHERE ticket IN (1,2,3)
This should work:
let list = [1, 2, 3]
let inPred = NSPredicate(format: "ticket IN %#", list)
var request = NSFetchRequest(entityName: "details")
request.returnsObjectsAsFaults = false
request.predicate = inPred
After that, call context.executeFetchRequest the usual way to retrieve the results.