CoreData attribute value based on other attributes - cocoa

I have a number of attributes for a CorData entity that are based on the values of other attributes. For example transactionTotalValue = transactionPrice * transactionQuantity. Currently I subclassed the NSManagedObject and created custom setters like this
- (void)setTransactionQuantity:(NSDecimalNumber *)transactionQuantity
{
[self willChangeValueForKey:#"transactionQuantity"];
[self setPrimitiveValue:transactionQuantity forKey:#"transactionQuantity"];
[self didChangeValueForKey:#"transactionQuantity"];
[self updateTotalValue];
}
- (void)setTransactionPrice:(NSDecimalNumber *)transactionPrice
{
[self willChangeValueForKey:#"transactionPrice"];
[self setPrimitiveValue:transactionPrice forKey:#"transactionPrice"];
[self didChangeValueForKey:#"transactionPrice"];
[self updateTotalValue];
}
- (void)updateTotalValue
{
self.transactionTotalValue = [self.transactionQuantity decimalNumberByMultiplyingBy:[NSDecimalNumber decimalNumberWithDecimal:[self.transactionPrice decimalValue]]];
}
Is this an acceptable way of doing this? if not what would be considered best practice for this situation?
The other alternative is to use KVO as follows
- (NSDecimalNumber *)transactionTotalValue
{
[self willAccessValueForKey:#"transactionTotalValue"];
NSDecimalNumber *total = [self.transactionQuantity decimalNumberByMultiplyingBy:[NSDecimalNumber decimalNumberWithDecimal:[self.transactionPrice decimalValue]]];
[self didChangeValueForKey:#"transactionTotalValue"];
return total;
}
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
NSSet *keypaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:#"transactionTotalValue"]) {
NSArray *affectingKeys = #[#"transactionQuantity", #"transactionPrice"];
keypaths = [keypaths setByAddingObjectsFromArray:affectingKeys];
}
return keypaths;
}
Is this the better option?

The attribute transactionTotalValue is to be completely dependent upon the two other attributes.
Declare it as a readonly, nonatomic property.
#property (nonatomic,readonly) id transactionTotalValue;
You can then implement the getter.
- (id)transactionTotalValue
{
//check for non-existent needed properties and handle here
return [self.transactionQuantity
decimalNumberByMultiplyingBy:[NSDecimalNumber
decimalNumberWithDecimal:[self.transactionPrice decimalValue]]];
}
Also override the class method for keys that affect the dependent property.
+ (NSSet *)keyPathsForValuesAffectingTransactionTotalValue
{
return [NSSet setWithObjects:#"transactionQuantity", #"transactionPrice", nil];
}
The transactionTotalValue property will be re-read as needed, say if you are updating a table source through bindings. By making it readonly you will be able to avoid any setter methods.

Related

NSCollectionView not updating when adding objects

I have an NSCollectionView with an array controller that is successfully showing objects after they are added and the application is restarted, but not when immediately when added.
I have a controller class which is a subclass of NSObject that reads data from a plist into an NSMutableArray, which is bound to an Array Controller, which is in turn bound to my NSCollectionView. I believe my bindings are correct, as if I add an object, after I restart my application, everything shows up fine, including the new object and all the bound attributes. But when I add an object, it won't be added immediately. The application needs to be restarted. I believe that since my bindings appear to be correct, this is an issue with my controller class not being Key-Value compliant.
I have implemented all of the methods I believe I should have, as per the "Key-Value Coding Accessor Methods" section of the Key-Value Coding programming guide. I believe I have implemented each of the required accessors in the [Collection Accessor Patterns for To-Many Properties][1] section. Furthermore, in the [Quick Start for Collection Views][2], which I have completed, it states not even all of these methods need to be implemented (which I have confirmed).
Here are some code samples to better explain what I am doing.
My collection class, "MYCollection":
#import <Foundation/Foundation.h>
#import "MyObject.h"
#interface MYCollection : NSObject
#property (retain, readwrite) NSMutableArray* objects;
- (void)insertObject:(MYObject *)object inObjectsAtIndex:(NSUInteger)index;
#end
#import "MYObjectCollection.h"
#import "MYObject.h"
#implementation MYObjectCollection
#synthesize objects = _objects;
- (id)init {
self = [super init];
if (self) {
_objects = [self objects];
}
return self;
}
- (NSArray*)objects {
// here I retrieve the objects from the plist into a mutable array
// let's call that array "sortedArray"
return sortedArray;
}
- (void)setObjects:(NSMutableArray *)objectsArray {
// here I write the object array to a plist
_objects = objectsArray;
}
-(void)insertObject:(MYObject*)object inObjectsAtIndex:(NSUInteger)index {
[_objects insertObject:object atIndex:index];
[self setObjects:_objects];
return;
}
-(void)addObjectsObject:(MYObject*)object {
[_objects addObject:object];
[self setObjects:_objects];
return;
}
-(void)removeObjectFromObjectsAtIndex:(NSUInteger)index {
[_objects removeObjectAtIndex:index];
[self setObjects:_objects];
return;
}
-(void)removeObjectsObject:(MYObject*)object {
[_objects removeObject:object];
[self setObjects:_objects];
return;
}
-(id)objectInObjectsAtIndex:(NSUInteger)index {
return [_objects objectAtIndex:index];
}
-(NSUInteger)countOfObjects {
return [_objects count];
}
- (NSEnumerator *)enumeratorOfObjects {
return [_objects objectEnumerator];
}
#end
I am adding objects to this controller by means of an external view, elsewhere:
MYObjectCollection *collection = [[MYObjectCollection alloc] init];
[collection insertObject:new inObjectsAtIndex:[collection.objects count]];
I'm not sure how to continue troubleshooting this issue. I believe that my bindings are correct and I think I have implemented all of the necessary methods for Key-Value coding, but maybe I haven't, or maybe they're wrong. Any help would be appreciated.

NSOutlineView (Source List) EXC_BAD_ACCESS error with ARC

I've an ARC enabled project and within IB I've created a window that holds the source list component which I believe is just a configured NSOutlineView. I'm using the magical delegate method:
- (id)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item
for which I cannot find any documentation for at all. Once this method is implemented the root node in my outline view will appear, upon which my entire model gets deallocated. Then when I try and expand the root node the app immediately crashes as model no longer exists.
If I don't use this method, my model remains, the source list works but none of the cells appear (understandably). I'm really not doing any thing fancy here at all.
I've never run into this sort of issue with ARC before, but it's late so there is a chance I've done something dumb and just can't see it. Here's the full code:
#implementation RLListController
- (void)awakeFromNib
{
RLPerson *stan = [[RLPerson alloc] initWithName:#"Stan"];
RLPerson *eric = [[RLPerson alloc] initWithName:#"Eric"];
RLPerson *ken = [[RLPerson alloc] initWithName:#"Ken"];
RLPerson *andrew = [[RLPerson alloc] initWithName:#"Andrew"];
RLPerson *daniel = [[RLPerson alloc] initWithName:#"Daniel"];
RLPerson *aksel = [[RLPerson alloc] initWithName:#"Aksel"];
[stan addChild:eric];
[stan addChild:ken];
[stan addChild:andrew];
[ken addChild:daniel];
[daniel addChild:aksel];
self.people = [#[stan] mutableCopy];
}
#pragma mark - Source List dataSource
- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
{
RLPerson *person = item;
return (item != nil) ? [person.children count] : [self.people count];
}
- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
{
RLPerson *person = item;
return (item != nil) ? [person.children count] > 0 : YES;
}
- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
{
RLPerson *person = item;
return (item != nil) ? [person.children objectAtIndex:index] : [self.people objectAtIndex:index];
}
- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
{
RLPerson *person = item;
return person.name;
}
- (id)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item
{
RLPerson *person = item;
NSTableCellView *cell = [outlineView makeViewWithIdentifier:#"DataCell" owner:self];
cell.objectValue = person;
[cell.textField setStringValue:person.name];
return cell;
}
#end
#implementation RLPerson
- (id)initWithName:(NSString *)name
{
self = [super init];
if(self)
{
_name = [name copy];
_children = [[NSMutableArray alloc] initWithCapacity:0];
}
return self;
}
- (void)addChild:(RLPerson *)child
{
[_children addObject:child];
}
- (void)dealloc
{
NSLog(#"dealloc");
}
#end
I've just figured out a similar crash in my code. I'll describe what the cause was for me... I'm pretty sure the same applies here, but I haven't tested your code.
Be aware that awakeFromNib can be called multiple times if you have multiple NIBs. I believe this is the case if you have NSTableCellView objects embedded within the NSOutlineView within your XIB file, which are extracted when you call makeViewWithIdentifier:owner: within outlineView:viewForTableColumn:item:.
Because you are creating your model objects (stan etc) within awakeFromNib, they are being recreated during these multiple calls. With each call, ARC is cleaning up the previous model objects, but NSOutlineView is still referencing them, hence the later crash when NSOutlineView tries to ask them for more information.
The fix is to move the model object creation out of awakeFromNib, perhaps into an init method instead.
Update:
Some other small points... it also took me a while to find the documentation for the magic outlineView:viewForTableColumn:item: method. For some reason, it is part of the NSOutlineViewDelegate protocol, not NSOutlineViewDataSource. I believe that if you implement this method, you don't need an implementation of outlineView:objectValueForTableColumn:byItem:.

Custom NSMangedObject accessor crashed NSOutlineView

I am thinking this is a bug in Core Data but before I file a bug report, I want to be sure it is not just me being stupid.
I set up an NSOutlineView to access the data of 5 different Core Data entities. Each entity's data is accessed with a different NSArrayController which is bound to the Entity and its' ManagedObjectContext. I then have the NSOutlineViewDataSource methods return the correct NSString object depending on which entity was expanded.
NOTE: entities is declared elsewhere as an NSArray with names for the entities.
- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index
ofItem:(id)item {
if(nil == item) {
return [entities objectAtIndex:index];
}
NSInteger entityIdx = [entities indexOfObject:item];
if (entityIdx == NSNotFound) {
return #"";
}
id returnObject = #"";
switch (entityIdx) {
case 0: {
Person *person = [[peopleArrayController arrangedObjects] objectAtIndex:index];
returnObject = person.fullName;
break;
}
case 1: {
Media *media = [[mediaArrayController arrangedObjects] objectAtIndex:index];
returnObject = media.imageTitle;
break;
}
case 2: {
Note *note = [[notesArrayController arrangedObjects] objectAtIndex:index];
returnObject = note.noteDescription;
break;
}
case 3: {
Source *source = [[sourcesArrayController arrangedObjects] objectAtIndex:index];
returnObject = source.title;
break;
}
case 4: {
Repository *repo = [[repostioriesArrayController arrangedObjects] objectAtIndex:index];
returnObject = repo.name;
break;
}
default:
break;
}
return returnObject;
}
The Person entity property fullName and the Media entity property imageTitle are custom accessors.
- (NSString *)fullName {
[self willAccessValueForKey:#"surName"];
[self willAccessValueForKey:#"givenName"];
NSString *firstName = [self valueForKey:#"givenName"];
NSString *lastName = [self valueForKey:#"surName"];
NSString *string = [NSString stringWithFormat:#"%# %#", (firstName) ? firstName : #"", (lastName) ? lastName : #""];
[self didAccessValueForKey:#"surName"];
[self didAccessValueForKey:#"givenName"];
return string;
}
- (id) imageTitle {
[self willAccessValueForKey:#"path"];
id title = [[self valueForKey:#"path"] lastPathComponent];
[self didAccessValueForKey:#"path"];
return title;
}
The program was crashing when I tried to expand the Person or the Media entities but not when I expanded the other entities. I traced the crash to [NSCell _setContents:][NSObject(NSObject) doesNotRecognizeSelector:]
I changed the Media property being returned to a standard Core Data accessor property #"path" and the program stopped crashing when I expanded the Media entity. So the problem is definitely related to the custom accessor.
FYI - I checked to make sure the entity was set to use the NSManagedObject class.
Can anyone give me a reason for the crash other than a bug?
I got around this problem by changing the method
- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
to return the managed object
case 1: {
Media *media = [[mediaArrayController arrangedObjects] objectAtIndex:index];
returnObject = media;
break;
}
Then I have the method
- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
return the String
if ([item isKindOfClass:[Media class]]) {
Media *media = (Media *)item;
return media.imageTitle;
}

NSTreeController: custom behavior for "canInsert" binding

I have a Cocoa app with an NSOutlineView managed by an NSTreeController.
In addition there's a button for adding new elements to the outline view. I bound the button's enabled flag to the tree controller's canInsert property.
I only want to allow adding up to 5 elements to the outline view. After that, canInsert should return NO.
I created my own sub-class of NSTreeController and overwrote canInsert, but the enabled status of the button does not change, because it doesn't realize that the tree controller has changed when adding elements.
I also implemented: keyPathsForValuesAffectingCanInsert and tried returning various properties such as content, arrangedObjects, but no luck here.
#implementation ILCustomTreeController
- (BOOL)canInsert
{
return [[self arrangedObjects] count] < 5;
}
+ (NSSet *)keyPathsForValuesAffectingCanInsert
{
return [NSSet setWithObject:#"content"]; // I also tried 'arrangedObjects'
}
#end
Here's a workaround that does work (although I still think this should be solved by using keyPathForValuesAffectingCanInsert). Suggestions are welcome.
#implementation ILCustomTreeController
- (BOOL)canInsert
{
return [[self arrangedObjects] count] <= 4;
}
- (void)addObject:(id)object
{
[self willChangeValueForKey:#"canInsert"];
[super addObject:object];
[self didChangeValueForKey:#"canInsert"];
}
- (void)insertObject:(id)object atArrangedObjectIndexPath:(NSIndexPath *)indexPath
{
[self willChangeValueForKey:#"canInsert"];
[super insertObject:object atArrangedObjectIndexPath:indexPath];
[self didChangeValueForKey:#"canInsert"];
}
- (void)remove:(id)sender
{
[self willChangeValueForKey:#"canInsert"];
[super remove:sender];
[self didChangeValueForKey:#"canInsert"];
}
#end

NSManagedObjectContext save causes NSTextField to lose focus

This is a really strange problem I'm seeing in my app. I have an NSTextField bound to an attribute of an NSManagedObject, but whenever the object is saved the textfield loses focus. I'm continuously updating the value of the binding, so this is far from ideal.
Has anyone seen anything like this before, and (hopefully) found a solution?
I encountered the issue recently and fixed it by changing the way the NSTextField was bound to the NSManagedObject attribute. Instead of binding the value of the text field to the selection.[attribute] key path of the NSArrayController, I bound the arrayController.selection.[attribute] keyPath of the view controller that had a proper outlet pointing to the controller.
For some reason, the NSTextField doesn't loose focus when the NSManagedObjectContext is saved if bound this way.
I want to share my solution. It will work for all fields without modification.
I have optimized it for this posting and removed some error checking, logging and thread safety.
- (BOOL)saveChanges:(NSError **)outError {
BOOL result = YES;
#try {
NSError *error = nil;
if ([self hasChanges]) {
// Get field editor
NSResponder *responder = [[NSApp keyWindow] firstResponder];
NSText *editor = [[NSApp keyWindow] fieldEditor: NO forObject: nil];
id editingObject = [editor delegate];
BOOL isEditing = (responder == editor);
NSRange range;
NSInteger editedRow, editedColumn;
// End editing to commit the last changes
if (isEditing) {
// Special case for tables
if ([editingObject isKindOfClass: [NSTableView class]]) {
editedRow = [editingObject editedRow];
editedColumn = [editingObject editedColumn];
}
range = [editor selectedRange];
[[NSApp keyWindow] endEditingFor: nil];
}
// The actual save operation
if (![self save: &error]) {
if (outError != nil)
*outError = error;
result = NO;
} else {
result = YES;
}
// Now restore the field editor, if any.
if (isEditing) {
[[NSApp keyWindow] makeFirstResponder: editingObject];
if ([editingObject isKindOfClass: [NSTableView class]])
[editingObject editColumn: editedColumn row: editedRow withEvent: nil select: NO];
[editor setSelectedRange: range];
}
}
} #catch (id exception) {
result = NO;
}
return result;
}
OK, so thanks to Martin for pointing out that I should read the docs a little more closely. This is expected behaviour, and here's what I did to get around it (use your judgement as to whether this is appropriate for you):
I save my context once every 3 seconds, checking at the start if the context has any changes before I bother executing the actual save: method on my NSManagedObjectContext. I added a simple incrementing/decrementing NSUInteger (_saveDisabler) to my Core Data controller class that is modified via the following methods:
- (void)enableSaves {
if (_saveDisabler > 0) {
_saveDisabler -= 1;
}
}
- (void)disableSaves {
_saveDisabler += 1;
}
Then all I do in my custom saveContext method is do a simple check at the top:
if (([moc hasChanges] == NO) || (_saveDisabler > 0)) {
return YES;
}
This prevents the save from occurring, and means that the focus is not stolen from any of my custom textfield subclasses. For completeness, I also subclassed NSTextField and enable/disable saves in my Core Data controller from the following methods:
- (void)textDidBeginEditing:(NSNotification *)notification;
- (void)textDidEndEditing:(NSNotification *)notification;
It might be a little messy, but it works for me. I'm keen to hear of cleaner/less convoluted methods if anyone has done this successfully in another way.

Resources