Using NSSegmentedControl with CoreData - cocoa

I have a Core Data app that works to add or remove one of a Client's many Appointments with buttons bound in IB to my appointments ArrayController. The appointments content is derived from whichever Client is selected in a feed list.
I wish to use a SegmentedControl, and as far as I could tell, this requires I programmatically add and remove the objects in appointments. I have successfully managed to add an Appointment using Marcus Zarra’s code from his book Core Data on p54, but I am at a loss to remove a selected Appointment. I am using a custom table cell, which I suspect might be complicating matters.
In short, I wish to programmatically achieve the equivalent of an ArrayController’s remove: method on a selected object.
Can anyone help, please?

Thanks, Martin. My code eventually looked like this.
-(IBAction) notesEditorSegClicked:(id)sender{
int clickedSegment = [sender selectedSegment];
switch (clickedSegment) {
case 0:{ // add new object
NSManagedObject *newNote = [NSEntityDescription
insertNewObjectForEntityForName:#"Note"
inManagedObjectContext:notes.managedObjectContext];
[notes addObject:newNote];
break;
}
case 1:{ // delete selected object
NSArray *objectsToDelete = [notes selectedObjects];
for (NSManagedObject* objectToDelete in objectsToDelete){
[notes.managedObjectContext deleteObject:objectToDelete];
}
break;
}
case 2:{// close view
[self loadClientSummary:sender];
break;
}
}
}

Get the current selection from you ArrayController bound to your UI
- (NSArray *)selectedObjects
delete those objects using the context
-(void) deleteObject:(NSManagedObject*) object
Sample:
NSArray* objectsToDelete = [NSArray arrayWithArray:[arrayController selectedObject]];
for (NSManagedObject* objectToDelete in objectsToDelete)
{
[arrayController.managedObjectContext deleteObject:objectToDelete];
}

Related

NSArrayController, creating CoreData entities programmatically, and KVO

I have an NSTableView whose NSTableColumn's value is bound to an NSArrayController. The arrayController controls a set of entities in my core data managed object context.
It works well, and when new entities are inserted into the arrayController via UI Actions the tableView selects the new item.
However I would to create new entities into the moc programmatically, then select the new object in the arrayController.
I have tried the following:
Image *newImage = [Image newImage]; // convenience method to insert new entity into mod.
newImage.title = [[pathToImage lastPathComponent] stringByDeletingPathExtension];
newImage.filename = [pathToImage lastPathComponent];
[self.primaryWindowController showImage:newImage];
The showImage: method is as so:
- (void)showImage:(Image *)image
{
[self.imagesArrayController fetch:self];
[self.imagesArrayController setSelectedObjects:#[image]];
}
However, the arrayController doesn't change its selection.
Am I doing it wrong? I assume that the newImage object that I created in the moc is the same as the object that the arrayController is controlling. If that's true, why isn't the arrayController changing its selection?
Hmm - testing that assumption, I have now checked the contents of the arrayController at runtime. The new image is not present - which I assume means that I have 'gone behind the back' of the bindings by manually inserting into the moc...
My newImage convenience method is as so:
+ (Image *)newImage
{
Image *newImage = [NSEntityDescription insertNewObjectForEntityForName:#"Image" inManagedObjectContext:[[CoreDataController sharedController] managedObjectContext]];
return newImage;
}
Is that not KVO compliant?
hmmm - Edit 2...
I assume that it is KVO compliant, as the new image appears in the UI. I'm now thinking that there is a delay between inserting the entity into the moc and the arrayController being informed.
I See from this question New Core Data object doesn't show up in NSArrayController arrangedObjects (helpfully shown to the right of this question by SO) that asking the arrayController to fetch: should help update the arrayController, but that the actual fetch: won't happen until the next time the runloop runs.
Should I delay the selection of the new object using a timer? That seems a little inelegant...
Right - solved it, thanks to this question: New Core Data object doesn't show up in NSArrayController arrangedObjects
I had to call processPendingChanges: on the moc directly after inserting the new object.
So, my new creation convenience method is now:
+ (Image *)newImage
{
Image *newImage = [NSEntityDescription insertNewObjectForEntityForName:#"Image" inManagedObjectContext:[[CoreDataController sharedController] managedObjectContext]];
[[[CoreDataController sharedController] managedObjectContext] processPendingChanges];
return newImage;
}
If you want to do this programmatically, the easiest is to directly add the new entity object to the NSArrayController, as easy as:
[self.imagesArrayController addObject:newImage];
That will do the trick of both adding the object to the controller, and selecting it.
One glitch though - I don't know what view (NSView, UIView) you use to present the NSArrayController's content - but NSTableView won't automatically scroll itself to reveal the newly added item.
I had to defer this (as adding happens in some later runloop) like thus:
NSUndoManager *um = self.managedObjectContext.undoManager;
[um beginUndoGrouping];
[um setActionName:NSLocalizedString(#"New Sample", NULL)];
PMWaterSample *sampleToAdd = [self createNewSample];
[self.samplesController addObject:sampleToAdd];
// Actual addition is deferred, hence we delay the scrolling too, on the main-thread's queue.
dispatch_async(dispatch_get_main_queue(), ^{
[self.samplesController rearrangeObjects];
// bring last row (newly added) into view
NSUInteger selectedIdx = [self.samplesController selectionIndex];
if (selectedIdx != NSNotFound) {
[self.samplesTable scrollRowToVisible:selectedIdx];
}
[um endUndoGrouping];
});
Hopefully that'll help. I never had to force the MOC to processPendingChanges. I still live with the feeling there's a better way to do this, and have the array controller make its embedding UI element scroll and reveal the new item, but I don't know how.

Correct way of updating an NSTableView in a Core Data app

I have a Core Data project with an NSTableView where the columns are bound to an NSArrayController. In turn, the controller's content is bound to the main managed object context of the AppDelegate.
I've sub-classed NSTextFieldCell's to provide a more customised way of displaying the data. Everything seems to work well but when I enumerate through the objects in the context and change an attribute, the NSArrayController doesn't seem to pass the new values through to the table. I know they're being changed as the sortDescriptors work and the table organises itself according to the new data but while still displaying the old values. I'm guessing this is down to the recycling of the NSCell's that display the data: although the underlying values have been changed in the Core Data DB they're not re-drawing.
After trying various KVO notifications with [myArrayController willChangeValueFor: aKeyPath] and the corresponding didChange as well as simply [myTable reloadData]. My only solution has been to unhook the managedObjectContext binding of the controller, and re-hook it after processing, like this:
self.hasBackgroundOp = YES;
self.isCancelled = NO; //can get changed with a 'cancel' button in the UI
if (self.managedObjectContext.hasChanges) {
[self. managedObjectContext save:nil];
}
[myArrayController setManagedObjectContext:nil]; //brutal un-hooking of controller
dispatch_group_t dispatchGroup = dispatch_group_create();
dispatch_group_async(dispatchGroup, dispatch_get_global_queue(0, 0), ^{
.
.
// create a thread context, set the persistent store co-ord
// and do some processing on the
// NSManagedObjects that are fetched into an array just
// for the purpose of this thread
.
.
[ctx save:&error];
//and after the processing is finished then refresh the table
dispatch_async(dispatch_get_main_queue(), ^{
[myArrayController setManagedObjectContext:self.managedObjectContext]; //this forces re-drawing of the table
self.hasBackgroundOp = NO;
self.isCancelled = NO;
});
});
This seems really brutal to be doing [myArrayController setManagedObjectContext:nil]; and then setting it again just to refresh the table's contents. I just can't believe I've done this correctly although it works just fine. Could anyone advise of a better way? Thanks!
UPDATE: actually setting the managed object context doesn't solve the problem. It seems to have stopped working. If the code is run twice in a row then the app just hangs. Comment out the -setManagedObjectContext: lines and the code can be run as many times as required.
In response to the comments, my custom cell uses the following to display itself:
NSInteger index = [[self stringValue] integerValue] + 1;
NSAttributedString *cellText = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:#"%lu",index] attributes:attributes];
NSRect textRect;
textRect.origin = cellFrame.origin;
textRect.origin.x += (cellFrame.size.width - cellText.size.width) / 2;
textRect.origin.y += (cellFrame.size.height - cellText.size.height) / 2;
textRect.size.width = cellText.size.width;
textRect.size.height = cellText.size.height;
[cellText drawInRect:textRect];
The array controller is setup in the main nib and has it's managed object context bound to the AppDelegate's. There are no fetch requests or predicates in the nib - a sort descriptor is bound in the AppDelegate's code:
[myArrayController setSortDescriptors:[NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:kINDEX ascending:YES]]];
Think I've found it - I added the following to the end of the threaded processing and executed it on the main thread:
[self.managedObjectContext reset];
[myArrayController fetch:self];
Resetting the MOC apparently clears it so forcing the controller to redraw the cells. The -fetch appears to be necessary otherwise the table goes blank...

xcode, reloading table view on exiting seque

I'm trying to reload the values of a table view after exiting from a seque. The process being: I perform the seque manually from the profile selection view, add a new profile name, return to the profile selection view. Then I would like to reload the table view adding the new profile name. It is running the code fine (same code as original entry into the scene), but I can't seem to get the native methods of numberOfRowsInSection and numberOfRowsInSection to repopulate the table view. I actually have to leave the screen and reenter it before the new profile name will update. Any thoughts?
//** performing seque manually
-(IBAction)buttonAddNewProfile:(id)sender
{
// creating object for profile selection screen
UIStoryboard *ProfileSelectionStoryboard=[UIStoryboard storyboardWithName:#"MainStoryboard" bundle:nil];
// creating object for add new profile storyboard
AddNewProfileViewController *addnewprofileVC=[ProfileSelectionStoryboard instantiateViewControllerWithIdentifier:#"Add New Profile"];
// setting the transition style
addnewprofileVC.modalTransitionStyle=UIModalTransitionStylePartialCurl;
// performing the segue
[self presentViewController:addnewprofileVC animated:YES completion:nil];
// performing new table view load on return from new profile
[self loadUsers];
}
//** function to load the new profile names in.
-(void)loadUsers
{
// retreiving the users from the database
SQLiteFunctions *sql = [[SQLiteFunctions alloc] init];
// testing for successful open
if([sql openDatabase:#"users"])
{
// setting query statement
const char *query = "SELECT * FROM users;";
// testing for that profile name existing already
if([sql getUserRecords:query] > 0)
{
// initializing array
NSMutableArray *names = [[NSMutableArray alloc] init];
// loop through object compling an array of user names
for(Users *ProfileUser in sql.returnData)
{
// adding user name to the listview array
[names addObject:ProfileUser.user_name];
}
// setting table view array to local array
tableData = names;
}
}
}
//** methods to reload the table view
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
{
// returning the number of rows in the table
return [tableData count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
{
// setting up the table view cells for data population
UITableViewCell *cell = nil;
cell = [tableView dequeueReusableCellWithIdentifier:#"MyCell"];
// testing for cell parameters
if (cell == nil)
{
// setting up cloned cell parameters
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"MyCell"];
}
// setting cell values to the array row value
cell.textLabel.text = [tableData objectAtIndex:indexPath.row];
// returning the current row label value
return cell;
}
You have a few different options here:
1) The easiest is to simply reload the table every time that the view controller is about to display its view:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.tableView reloadData];
}
The downside though, is that this will be executed every time that the view is displayed, even when you don't necessarily need to reload the data.
2) If you are using storyboard's and targeting iOS 6+ then you can use an unwind segue to call a specific method on your view controller when going back from the add profile view controller. For more info, see this SO question/answers: Does anyone know what the new Exit icon is used for when editing storyboards using Xcode 4.5?
3) If you are targeting older versions of iOS or aren't using storyboards, then you can create a protocol with a method that should be called whenever a new profile is added and you can reload the data whenever that method is called. There are lots of questions here on SO which cover how to do this (like dismissModalViewController AND pass data back which shows how to pass data, but you can do the same thing to just call a method).
This the actual answer from lnafzinger in the comments above. Thanks again.
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.tableView reloadData];
}
It took me a bit to figure this out, but it is because you are using
the UIModalTransitionStylePartialCurl modalTransitionStyle. I guess
they don't call it because it doesn't completely leave the screen. If
you use a different style (like the default) then it will call
viewWillAppear. If you want to stay with that, then you should
probably use one of the other methods. – lnafziger

How to bind NSButton enabled state to a composed condition

This is my situation in Xcode Interface Builder:
There is also an NSArrayController in entity mode which controls the content of the NSTableView. I want to enable the 'Create' button when the NSTableView is empty (as controlled by the NSSearchField) AND when the text in the NSSearchField is not empty. How do I achieve that? Is it possible without programming?
To what KVO compliant values can I bind the 2 enabled conditions of the 'Create' button?
I don't think there's a way to do it entirely in interface builder, but with a small amount of code you can get it working pretty easily. First, make sure your controller (or App Delegate) is set as the delegate of the search field, and that it has IBOutlet connections to the search field, the button and the array controller. Here's how I would implement it:
// This is an arbitrary pointer to indicate which property has changed.
void *kObjectsChangedContext = &kObjectsChangedContext;
- (void)awakeFromNib {
// Register as an observer so we're notified when the objects change, and initially at startup.
[arrayController addObserver:self
forKeyPath:#"arrangedObjects"
options:NSKeyValueObservingOptionInitial
context:kObjectsChangedContext];
}
// This updates the button state (based on your specs)
- (void)updateButton {
BOOL canCreate = (searchField.stringValue.length > 0 &&
0 == [arrayController.arrangedObjects count]);
[createButton setEnabled:canCreate];
}
// This delegate method is called whenever the text changes; Update the button.
- (void)controlTextDidChange:(NSNotification *)obj {
[self updateButton];
}
// Here's where we get our KVO notifications; Update the button.
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (kObjectsChangedContext == context)
[self updateButton];
// It's good practice to pass on any notifications we're not registered for.
else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
If you're new to bindings some of that may look like Greek, hopefully the comments are clear enough.
I'm SOOO late for this, but came up with another method and just tested it in my app. It works, so I'm going to share it for anyone who will find this question in the future.
Basically what you want to do is to create a property WITHOUT a corresponding value in your controller
#property (readonly) BOOL enableProperty;
This means that there's actually no
BOOL enableProperty;
defined in the header file, or anywhere
then, rather than synthesize it, just write your own getter, and put there your condition
- (BOOL) enableProperty{
return (condition);
}
Third step: anytime there's the chance that your condition changes, notify it.
- (void) someMethod{
//.... Some code
[self willChangeValueForKey:#"enableProperty"];
[Thisline mightChange:theCondition];
[self didChangeValueForKey:#"enableProperty"];
//.... Some other code
}
fourth step: in IB, bind your control's enabled property to this "fake" property.
Enjoy! ;)
You seems to have a window, so presumably you have a controller object which is set as the File's Owner for the NIB file.
Why not declare a boolean property in this controller class, that returns a value based whatever conditions you want ?
#property(readonly) BOOL canCreate;
That you implement :
-(BOOL)canCreate {
// compute and return the value
}
Be sure to send KVO notifications appropriately when the conditions for the creation change.
The last step is to bind the button's enabled binding on the File's Owner canCreate key.

Custom NSView in NSMenuItem not receiving mouse events

I have an NSMenu popping out of an NSStatusItem using popUpStatusItemMenu. These NSMenuItems show a bunch of different links, and each one is connected with setAction: to the openLink: method of a target. This arrangement has been working fine for a long time. The user chooses a link from the menu and the openLink: method then deals with it.
Unfortunately, I recently decided to experiment with using NSMenuItem's setView: method to provide a nicer/slicker interface. Basically, I just stopped setting the title, created the NSMenuItem, and then used setView: to display a custom view. This works perfectly, the menu items look great and my custom view is displayed.
However, when the user chooses a menu item and releases the mouse, the action no longer works (i.e., openLink: isn't called). If I just simply comment out the setView: call, then the actions work again (of course, the menu items are blank, but the action is executed properly). My first question, then, is why setting a view breaks the NSMenuItem's action.
No problem, I thought, I'll fix it by detecting the mouseUp event in my custom view and calling my action method from there. I added this method to my custom view:
- (void)mouseUp:(NSEvent *)theEvent {
NSLog(#"in mouseUp");
}
No dice! This method is never called.
I can set tracking rects and receive mouseEntered: events, though. I put a few tests in my mouseEntered routine, as follows:
if ([[self window] ignoresMouseEvents]) { NSLog(#"ignoring mouse events"); }
else { NSLog(#"not ignoring mouse events"); }
if ([[self window] canBecomeKeyWindow]) { dNSLog((#"canBecomeKeyWindow")); }
else { NSLog(#"not canBecomeKeyWindow"); }
if ([[self window] isKeyWindow]) { dNSLog((#"isKeyWindow")); }
else { NSLog(#"not isKeyWindow"); }
And got the following responses:
not ignoring mouse events
canBecomeKeyWindow
not isKeyWindow
Is this the problem? "not isKeyWindow"? Presumably this isn't good because Apple's docs say "If the user clicks a view that isn’t in the key window, by default the window is brought forward and made key, but the mouse event is not dispatched." But there must be a way do detect these events. HOW?
Adding:
[[self window] makeKeyWindow];
has no effect, despite the fact that canBecomeKeyWindow is YES.
Add this method to your custom NSView and it will work fine with mouse events
- (void)mouseUp:(NSEvent*) event {
NSMenuItem* mitem = [self enclosingMenuItem];
NSMenu* m = [mitem menu];
[m cancelTracking];
[m performActionForItemAtIndex: [m indexOfItem: mitem]];
}
But i'm having problems with keyhandling, if you solved this problem maybe you can go to my question and help me a little bit.
Add this to your custom view and you should be fine:
- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent
{
return YES;
}
I added this method to my custom view, and now everything works beautifully:
- (void)viewDidMoveToWindow {
[[self window] becomeKeyWindow];
}
Hope this helps!
I've updated this version for SwiftUI Swift 5.3:
final class HostingView<Content: View>: NSHostingView<Content> {
override func viewDidMoveToWindow() {
window?.becomeKey()
}
}
And then use like so:
let item = NSMenuItem()
let contentView = ContentView()
item.view = HostingView(rootView: contentView)
let menu = NSMenu()
menu.items = [item]
So far, the only way to achieve the goal, is to register a tracking area manually in updateTrackingAreas - that is thankfully called, like this:
override func updateTrackingAreas() {
let trackingArea = NSTrackingArea(rect: bounds, options: [.enabledDuringMouseDrag, .mouseEnteredAndExited, .activeInActiveApp], owner: self, userInfo: nil)
addTrackingArea(trackingArea)
}
Recently I needed to show a Custom view for a NSStatusItem, show a regular NSMenu when clicking on it and supporting drag and drop operations on the Status icon.
I solved my problem using, mainly, three different sources that can be found in this question.
Hope it helps other people.
See the sample code from Apple named CustomMenus
In there you'll find a good example in the ImagePickerMenuItemView class.
It's not simple or trivial to make a view in a menu act like a normal NSMenuItem.
There are some real decisions and coding to do.

Resources