Cocoa Bindings: Binding to the "many" end of a to-many relationship - core-data

Using the Employees-Departments example what I want to do is bind a column to "Departments.arrangedObjects.employees.#sum.hoursWorked" as outlined below:
Entity Employee
attributes: "firstName", "lastName", "hoursWorked"
relationship: "departments"
Entity Department
attributes: "name"
relationship: "employees"
I want a table which will display some summary info about departments.
I bind the first column to my "Departments" array controller, "arrangedObjects.name".
I can have a column displaying the number of employees in a department by binding to "arrangedObjects.employees.#count"
However I can't get a sum of the hoursWorked by employees as I assume I might by binding to "arrangedObjects.employees.#sum.hoursWorked"
The error I get is along the lines of "[<_NSFaultingMutableSet 0x1acea0> addObserver:forKeyPath:options:context:] is not supported. Key path: #sum.hoursWorked"
I believe this is because it is not possible to bind to the many end of a to-many relationship. If so how can I do what I want to do?
For extra credit, say each employee also has another attribute, "race", I would also like my summaries table to show the number of unique races in each department.
Thanks in advance.

I encountered the same errors you did. It seems that while you can get the set of employees and perform some set of aggregate operations on it by doing something like:
Department* dept = ;
NSSet* employees = dept.employees;
NSNumber* sumOfHoursWorked = [employees valueForKeyPath: #"#sum.hoursWorked"];
There's a difference when you bind. Bindings are asking to observe the key path, not evaluate it once. Given that, it kinda, sorta makes sense why you can't bind to these key paths. Kinda. Sorta.
Now. As for a solution, what I usually do in cases like this is write a tiny little NSValueTransformer subclass to do just what I need, and then plug that into IB. This way, I write the ten lines of code I need, but don't end up doing the whole NSTableView data source spiel for want of a simple aggregate. In this case, you might do something like this:
// Declaration
#interface MySumOfHoursWorkedTransformer : NSValueTransformer
#end
#interface MyNumberOfRacesTransformer : NSValueTransformer
#end
// Implementation
#implementation MySumOfHoursWorkedTransformer
+ (Class)transformedValueClass { return [NSNumber class]; } // class of the "output" objects, as returned by transformedValue:
+ (BOOL)allowsReverseTransformation { return NO; } // flag indicating whether transformation is read-only or not
- (id)transformedValue:(id)value // by default returns value
{
NSNumber* retVal = nil;
if ([value isMemberOfClass: [Department class]])
{
double hoursWorked = 0.0;
for (Employee* employee in [value valueForKey: #"employees"])
{
NSNumber* hoursWorkedNumber = employee.hoursWorked;
hoursWorked += hoursWorkedNumber ? [hoursWorkedNumber doubleValue] : 0.0;
}
retVal = [NSNumber numberWithDouble: hoursWorked];
}
return retVal;
}
#end
#implementation MyNumberOfRacesTransformer
+ (Class)transformedValueClass { return [NSNumber class]; } // class of the "output" objects, as returned by transformedValue:
+ (BOOL)allowsReverseTransformation { return NO; } // flag indicating whether transformation is read-only or not
- (id)transformedValue:(id)value // by default returns value
{
NSNumber* retVal = nil;
if ([value isMemberOfClass: [Department class]])
{
NSMutableSet* raceSet = [NSMutableSet set];
for (Employee* employee in [value valueForKey: #"employees"])
{
id raceVal = employee.race;
if (raceVal)
[raceSet addObject: raceVal];
}
retVal = [NSNumber numberWithUnsignedInteger: raceSet.count];
}
return retVal;
}
#end
Then, just bind those TableColumns to ArrayController.arrangedObjects and plug in the appropriate value transformer subclass. Now, you won't be able to edit those values, but what would it mean to edit an aggregate value anyway?
Hope that helps. I've used this approach a bunch, and it sure beats giving up on bindings.

Related

Fetching objects form core data by IndexPath

I have a TableView. By tapping an entry in this table, another view opens in which that entry is editable.
What's the proper way to handle this? I am aware that I can fetch entries from Core Data via predicates like this:
func fetchEntriesStartingWith(startingDate:Date) -> [FuelEntry] {
let fetchRequest =
NSFetchRequest<NSManagedObject>(entityName: "FuelEntry")
fetchRequest.predicate = NSPredicate(format: "date >= %#", startingDate as CVarArg)
let sort = NSSortDescriptor(key: #keyPath(FuelEntry.date), ascending: false)
fetchRequest.sortDescriptors = [sort]
do {
return try (CoreDataHandler.managedContext.fetch(fetchRequest) as? [FuelEntry] ?? [FuelEntry]())
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
return []
}
}
However, this seems error prone: What if I have two entries with the exact same values (might happen in my application) and thus, the NSFetchRequest returns two entries?
Obviously, those two entries will have different IDs, but I'm unsure how to work with those and infer which entry will correspond to what indexPath.

Dialog RunBase custom lookup: Alt + Down key combination doesn't work

MS Dynamics AX 4.0
I have a class with a dialog that extends RunBase, a dialogField of Range type and a custom lookup for it. It works as planned but one thing upsets me.
Normal lookup opens on Alt + Down key combination, but it doesn't work in my dialog. I assume this is because "Range" EDT is not related to any TableField.
But I have my own lookup, can I force it somehow to drop down on Alt + Down?
Here is my dialog method:
protected Object dialog(DialogRunBase dialog, boolean forceOnClient)
{
Object ret;
;
ret = super(dialog, forceOnClient);
dialogFld = new DialogField(ret, typeid(Range), 100);
dialogFld.init(ret);
dialogFld.lookupButton(FormLookupButton::Always);
dialogFld.fieldControl().replaceOnLookup(false);
return ret;
}
Here is my lookup, as you can see, it's based on ItemId EDT:
protected void Fld100_1_Lookup()
{
TableLookup_RU sysTableLookup = new TableLookup_RU();
Query query = new Query();
FormRun lookupForm;
QueryBuildDataSource qbds = query.addDataSource(tablenum(InventTable));
;
sysTableLookup.parmTableId(tablenum(InventTable));
sysTableLookup.parmCallingControl(dialogFld.fieldControl());
sysTableLookup.addLookupfield(fieldnum(InventTable, ItemId));
sysTableLookup.addLookupfield(fieldnum(InventTable, ItemName));
findOrCreateRange_W(qbds, fieldnum(InventTable, ItemType), SysQuery::valueNot(ItemType::Service));
sysTableLookup.parmQuery(query);
lookupForm = sysTableLookup.formRun();
dialogFld.fieldControl().performFormLookup(lookupForm);
}
And dialogPostRun:
public void dialogPostRun(DialogRunbase dialog)
{
;
dialog.formRun().controlMethodOverload(true);
dialog.formRun().controlMethodOverloadObject(this);
super(dialog);
}
This problem is not that critical, but it bothers me. If someone could help, I'd be really grateful.
P.S.: I could use ItemId typeId, but I need to append many items, and ItemId is only 20 chars long..
I've discovered that I don't have to use Range typeid for the dialogField. dialogField.limitText(int) works just fine, it overrides the length of EDT. So I changed dialog method like this:
protected Object dialog(DialogRunBase dialog, boolean forceOnClient)
{
Object ret;
;
ret = super(dialog, forceOnClient);
dialogFld = new DialogField(ret, typeid(ItemId), 100); //if typeId doesn't have relations Alt + Down doesn't work
dialogFld.init(ret);
dialogFld.label("#SYS72708");
dialogFld.lookupButton(FormLookupButton::Always);
dialogFld.limitText(200);
dialogFld.fieldControl().replaceOnLookup(false);
return ret;
}
Create a new extended data type ItemIdRange, extend from Range.
Be sure to set the relation on the new type to relate to InventTable.ItemId to get automatic lookup.
Also the form control must have property ReplaceOnLookup set to no, to allow the user to add more criteria. For a DialogRunbase field this may be done this way:
FormStringControl fsc = dialogField.control();
fsc.replaceOnLookup(false);
The code posted in the question is then not needed.

Swift set or update asset in core data

Two Entities
Gymnast is one to many to Meet
I would like to when I save a new meet, it gets assigned to as a meet to each gymnast where they can then score their individuals scores for each event
Maybe I completely wrong in my logic, but here is what I am trying to do
let request = NSFetchRequest(entityName: "Gymnast")
do {
let entities = try AD.managedObjectContext.executeFetchRequest(request) as! [Gymnast]
for item in entities {
if let first = item.valueForKey("firstName"), last = item.valueForKey("lastName") {
print("Name: \(first) \(last)")
let myMeet = NSEntityDescription.insertNewObjectForEntityForName("Meet", inManagedObjectContext: AD.managedObjectContext) as! Meet
myMeet.meetName = "Winter Classic"
let myMeets = item.meets!.mutableCopy() as! NSMutableSet
myMeets.addObject(myMeet)
item.meets = myMeets.copy() as? NSSet
AD.saveContext()
}
}
} catch {
}
}
}
I think it is not ideal to replicate the Meet object over and over again for each Gymnast. For example, the meetName will be stored multiple times. I am not sure this is intended.
However, going with your setup, your problem is how you assign the to-many relationship. For a one-to-many it is always easier to simply set the to-one relationship. (Remember, there is always a reverse relationship in the Core Data model.)
Thus,
myMeet.gymnast = item
is all you need.
To add and remove to-many relationships, you can use this extension:
// Support adding to many-to-many relationships
extension NSManagedObject {
func addObject(value: NSManagedObject, forKey key: String) {
let items = self.mutableSetValueForKey(key)
items.addObject(value)
}
func removeObject(value: NSManagedObject, forKey key: String) {
let items = self.mutableSetValueForKey(key)
items.removeObject(value)
}
}

Retrieving company name from HMService and/or HMAccessory object instance

I am using Home Kit Accessory simulator and I'd like to retrieve the company name of an accessory from an instance of HMService. However, when I add a breakpoint I cannot see any field related to the company name (I searched in both HMService and HMAccessory).
Any suggestion?
You can get name of Manufacturer from HMServiceTypeAccessoryInformation service, Service contains characteristic array in this there is HMCharacteristicTypeManufacturer characteristic.
You can use this to display name of company.
- (HMCharacteristic *)characteristicForAccessory:(HMAccessory *)accessoryValue{
HMAccessory *thisAccessory = accessoryValue;
HMService *service;
for (HMService *thisService in thisAccessory.services) {
if([thisService.serviceType isEqualToString:HMServiceTypeAccessoryInformation]) {
service = thisService;
}
}
HMCharacteristic *characteristic;
if (service) {
for (HMCharacteristic *charact in service.characteristics) {
if ([charact.characteristicType isEqualToString:HMCharacteristicTypeManufacturer]) {
characteristic = charact;
}
}
}
return characteristic;
}
Use Characteristic object's value property to get name of manufacturer.
Like characteristic.value
Take a look at Raeid Saqur's RSHomeKit framework:
You can get accessory by calling service.accessory. Then use:
+ (NSString *)getManufacturerNameForHMAccessory:(HMAccessory *)accessory;
+ (NSString *)getManufacturerNameForHMAccessory:(HMAccessory *)accessory {
if (!accessory) {
return nil;
}
HMCharacteristic *manufacturer = [HomeKitUtility getCharacteristicWithUUID:HMCharacteristicTypeManufacturer forAccessory:accessory];
if (manufacturer && manufacturer.value) {
return (NSString *)manufacturer.value;
}
return nil;
}

Reordering UITableView with NSOrderedSet in CoreData

Does anyone has sample code for ordering in UITableView using NSOrderedSet?
Had read many articles about reordering, but still don't understand how to do this in iOS5.
Hi i implemented it like this. The "currentObject" is root object of the relationship and "subItems" the name of the ordered relationship in the model which is managed by the UITableViewController.
- (void)moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath
{
NSMutableOrderedSet* orderedSet = [self.currentObject mutableOrderedSetValueForKey:#"subItems"];
NSInteger fromIndex = fromIndexPath.row;
NSInteger toIndex = toIndexPath.row;
// see http://www.wannabegeek.com/?p=74
NSIndexSet *indexes = [NSIndexSet indexSetWithIndex:fromIndex];
if (fromIndex > toIndex) {
// we're moving up
[orderedSet moveObjectsAtIndexes:indexes toIndex:toIndex];
} else {
// we're moving down
[orderedSet moveObjectsAtIndexes:indexes toIndex:toIndex-[indexes count]];
}
[self.dataStore saveObjectContext];
}

Resources