NSArrayController + Core Data + Bindings - objective-c

I have one window containing an NSTableView listing a bunch of managed objects. I can right click a selection of those objects, and bring up an edit window to edit the properties of those objects (in theory).
Here's my code to bring up the window:
EditObjController *editObjController = [[EditObjController alloc] init];
// This line has to be first or the properties of editObjController are all nil.
[[editObjController window] makeKeyAndOrderFront:self];
[editobjController.arrayController setManagedObjectContext:[[NSApp delegate] managedObjectContext]];
NSArray *selection = [_objects objectsAtIndexes: [_objectsTableView selectedRowIndexes]];
[editObjController.arrayController setContent: selection];
My nib for the editing window has an NSArrayController in it. I have various textfields on the window, all of which are bound to the NSArrayController, controller key 'selection', with model key path=nameoffield.
The array controller is a valid object. No errors are reported.
All that comes up when the window appears is 'No selection' in all the textfields. What am I missing? I've googled extensively and I can't find a decent tutorial that explains how to actually edit records using Core Data and Bindings.

Your NSTableView and Edit Window should share the same array controller.

Related

NSBrowser selection of row and action of NSBrowserCell

I am having an NSBrowser in one of my windows. It has a checkbox, image and textbox as you see in the screenshot.
I am struggling to do two things:
Change the row selection color. By default it is blue.
Action on the checkbox
The checkbox + image + textbox is added to the subclass of NSBrowserCell like this:
- (instancetype)init
{
self = [super init];
if (self) {
_buttonCell = [[NSButtonCell alloc] init];
[_buttonCell setButtonType:NSButtonTypeSwitch];
[_buttonCell setTarget:self];
[_buttonCell setAction:#selector(cellButtonClick:)];
[_buttonCell setTitle:#""];
_imageCell = [[NSImageCell alloc] init];
_textCell = [[NSTextFieldCell alloc] init];
}
return self;
}
I've added target and Action too, but it is not called.
How do I achieve these two things?
Any guidance will be appreciated.
Caveat: customizing NSBrowser/NSBrowserCell is usually hard. It's a legacy view class that's been essentially abandoned for years, and it has a lot of undocumented quirks.
Having said that, try overriding -highlightColorWithFrame:inView: to use a different color for highlighting the row. It that's all you need, then I think that should do it.
As for the action, NSCell (unlike views and controls) is not a subclass of NSResponder and doesn't perform any automatic mouse event processing. (Cells are just helper objects to draw the view of a control/element.)
You'll probably have to catch the event at the browser view level, then perform hit testing to find the column/row that contains your check box cell. If you find a hit, then post the action message yourself (-sendAction:to:from:)—which is literally what a NSControl view does.

Adding Subviews to NSView show up, but can not be removed

In my application window I have two NSViews. On the left the NSView ("Menu") contains a few buttons. When one of the buttons is clicked it should change the contents of the right NSView ("Content").
For each of the views on the right I have a separate NSViewControllers that get loaded and their views gets added as a subview. When a further button gets pressed on the left the added subviews on the right should be removed and the new view should be loaded as a subview.
To accomplish this I load my Menu in AppDelegate with the following:
MenuVC *menuSubView = [[MenuVC alloc] initWithNibName:#"MenuVC" bundle: nil];
menuSubView.contentView = (NSView*)[self contentView];
[[self menuView] addSubview:[menuSubView view]];
This works fine. As you can see I have a NSView pointer in the Menu VC which points to the contentView so that I can populate it with the subviews.
Now as a method for one of the button presses I do the following:
SomeContentVC *subView = [[SomeContentVC alloc] initWithNibName:#"SomeContentVC" bundle:nil];
[self.contentView addSubview:[subView view]];
This does not work.
If I however add a subview from the awakeFromNib method of the MenuViewController implementation (in the case of default content when the app opens) it works. However when I try to remove that subview using
[[self.contentView setSubviews:[NSArray array]];
I can't. Interesting is also that if I try to count the number of subviews (even after having added one in the awakeFromNib method) it returns 0 subviews for self.contentView. Why? How can I get it to work properly?
Thanks
The fact that messaging self.contentView achieves nothing except, for some things, returning 0 probably means that self.contentView is nil.
Do you perhaps have two instances of MenuVC by accident? Perhaps one instantiated in a NIB and one instantiated in code?
When in doubt, log everything. Log self in various methods. Log menuSubView just after you create it. Log menuSubView.contentView just after you assign it. Etc. Eventually, you'll probably see that you're interacting with different objects than you thought you were.

passing data/objects/moc between viewcontrollers - best practice

I have a scenario that i suspect is very common, i've found various ideas in responses to other similar questions including setting up IBOutlets, passing NSmanagedobjects as properties and just using one view controller but swapping out the views but I'm uncertain as to which would be the best idea to implement for my solution.
I have an iOS/iphone app that has a typical model. UITabBarController containing multiple UINavigationControllers.
In one of the navigation controllers i have a viewcontroller with a tableView in it. This tableView is populated from fetched core Data entity is called 'tags'. When I set the tableView up with a UIBarButtonItem (Add) the add button needs to behave as follows: I need it to create a new 'tag' object for that entity and populate the object with some known data and then prompt the user to drill down 3 levels of classification (Region -> area -> topic) to reach a topic object. i then need to add that 'topic' object to a relationship of the original newly added 'tag' object (3 views further down the stack). - hope thats clear.
(there's lots of code I'm not sure which bits to present so i'm describing for now with the main snippet, by all means let me know any specific code you'd like to see.)
- (void)addTag {
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
NSEntityDescription *entity = [[self.fetchedResultsController fetchRequest] entity];
NSManagedObject *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];
[newManagedObject setValue:#"(untitled)" forKey:#"tagID"];
NSError *error;
if (![context save:&error])
NSLog(#"Error saving entity: %#", [error localizedDescription]);
ChooseRegion *aView = [[[ChooseRegion alloc] init] autorelease];
aView.theTag = newManagedObject;
[self.navigationController pushViewController:aView animated:YES];
}
You can see I've created the object, set one of it's properties a value, saved it and passed the object to the next view (chooseRegion) which is the first of 3 views. on each the didSelectRowAtIndexPath code passes the selected row and this 'tag' object to the next view, i'm not sure whether this is right because i'm not sure what to do when i get to the topic object on the 3rd view??? i'm lost.
Always more than one way to skin a cat but what approach should i be tackling this from. Should I be passing the 'tag' object forward and save it at the 3rd view or pass back the selected topic and save it at the original view which created the 'tag' object?
Many thanks in advance.
I would use notifications for this. Notifications are a simple way to decouple parts of the application, but still have them work together.
In your case, the moment the user selects the tag object in the 3rd view, I would send a notification as such:
[[NSNotificationCenter defaultCenter] postNotificationName:#"tagSelected" object:myTag];
Now, in the controller that has the "add" button, make it subscribe to that event:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(handleTagSelected:) name:#"tagSelected" object:nil];
Make sure you implement the handleTagSelected: method, and in that method you can get the tag object and close the drilldown view that you have open:
- (void)handleTagSelected:(NSNotification *)notification {
Tag *mytag = (Tag *)notification.object;
[self dismissModalViewControllerAnimated:YES];
}
Then you can do whatever you want with the tag.

Reusable bits of interface, designed in IB

I'm making an app that includes the same group of buttons in many different contexts. The buttons send their actions to a different object in each context. I'd like to be able to design a single NSView in IB containing the buttons, and then be able to put copies of that view in many places in my nibs, while maintaining the link, so changes propagate. I'd like to connect each of those instances to different objects, and have the buttons send their actions to whatever object their parent view is connected to.
I thought of creating a subclass of NSView which, when loaded, replaces itself with another view which it loads from a nib file, setting the connected object as File's Owner, but I'm not convinced this is the cleanest method. Here's my implementation of that idea (which -does- work):
#implementation AVNViewFromNib
- (void)awakeFromNib
{
//Load the nib whose name is specified by the "nibFile" key
NSNib* viewNib = [[NSNib alloc] initWithNibNamed:[self valueForKey:#"nibFile"] bundle:[NSBundle mainBundle]];
NSMutableArray* topLevelObjects = [NSMutableArray new];
[viewNib instantiateNibWithOwner:relatedObject topLevelObjects:&topLevelObjects];
//Find our replacement view in that nib
for (id currentObject in topLevelObjects)
{
if ([currentObject isKindOfClass:NSClassFromString(#"AVNReplacementView")])
{
representedView = currentObject;
break;
}
}
//Copy appropriate properties from us to our representedView
[representedView setAutoresizingMask:[self autoresizingMask]];
[representedView setFrame:[self frame]];
[[self superview] addSubview:representedView];
//We were never here. :)
[self removeFromSuperview];
[viewNib autorelease];
}
#end
#implementation AVNReplacementView
#end
Is that the cleanest method? Is there a standard way of going about this?
You can create the view with the buttons in it in IB, then drag that view into the Library window and save it. The catch is, there's no “link” between them; editing one won't change anything about the others.
If you want that, you'll need to make a subclass of NSView instead.
I thought of creating a subclass of NSView which, when loaded, replaces itself with another view which it loads from a nib file, setting the connected object as File's Owner, but I'm not convinced this is the cleanest method.
That could work. I don't think that's really all that dirty; the reason init methods return an object is that they explicitly can return a different object. However, I'm not sure how you'd handle views of different frames, since the loaded view will have whatever frame it has in the nib.
Another way would be to load the buttons from a nib, but you'd have to adjust their frames before adding them as subviews.

NSObjectController - setAutomaticallyPreparesContent:

I have a small core data app. I have a subclass of an NSObjectController acting as the binding between the view and the model and a NSTextField on the view for the user to type into.
When the window opens the text field is editable because I have the bindings set to my subclassed NSObjectController, controller key to "content" and the Model Key Path to an attribute of my Entity.
If I type in some text, save the file and reopen it the text in the NSTextField isn't there.
For testing, I have a button connected to the add: selector of the controller and when you press the button everything works fine - you can enter text into a NSTextField, you can save the document, you can open it again.
How do I prepare the object when the nib loads?
In my init method in my subclass of the NSObjectController I have:
[self setAutomaticallyPreparesContent:YES];
and then I have in MyDocument:windowControllerDidLoadNib (oc is the IBOutlet to the subclassed objectcontroller in IB):
[oc fetchWithRequest:nil merge:NO error:&error];
but it didn't work. I need to create the content so the user can get started typing.
Thanks
OK, here's my contribution. I'm happy to be able to answer something! I found this in the "NSPersistentDocument Core Data Tutorial" in the documentation.
Remember, my problem was when the document is created I want to create an NSManagedObject. That way the user doesn't have to hit the "add" button. I don't want the document to be dirty (until the user types something) and I don't want to replace the content if I'm opening a saved file. This needs to happen only when a new document is created.
NSDocument provides a method:
initWithType:error:
to accomplish this.
1) grab the managedObjectContext,
2) turn off undo for the moment. This prevents the doc from being dirty and preventing the user from undoing the creation and insertion of the Entity.
3) use insertNewObejctForEntityForName:inManagedObjectContext
4) install the changes
5) turn back on undo
Here's the code:
- (id)initWithType:(NSString *)typeName error:(NSError **)outError
{
self = [super initWithType:typeName error:outError];
if (self != nil) {
NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
[[managedObjectContext undoManager] disableUndoRegistration];
self.myManagedObject = [NSEntityDescription insertNewObjectForEntityForName:#"myEntityName"
inManagedObjectContext:managedObjectContext];
[managedObjectContext processPendingChanges];
[[managedObjectContext undoManager] enableUndoRegistration];
NSLog(#"initWithType");
}
return self;
}