Commit NSTextField changes before saving - objective-c

I have an NSDocument app and I would like to have a NSTextField to commit the current changes to the model every time the user save (through cmd+s for example).
I don't use binding and at the moment the changes are pushed to the model in the -controlTextDidEndEditing: method. Calling the [window makeFirstResponder:nil] does push the changes to the model but also causes the control to lose focus which is not really a reasonable behaviour.
Googling around I have seen that several people suggested to use the -commitEditing method but it only applies to bindings, am I wrong?

You can just call your already defined controlTextDidEndEditing: from your save action:
-(IBAction)save:(id)sender
{
[self controlTextDidEndEditing: ...]
}
to trigger the same code you already wrote!

Related

Define a controller for NSDocument for document-based application

I'm not very sure how Document-Based Applications works.
I've created some actions for NSObject in the Mainmenu.xib. One of this is called when the user click on "File>new":
-(IBAction) newDocument:(id)sender{
Document* newDoc =[[Document alloc] init];
[[NSDocumentController sharedDocumentController]addDocument:newDoc];
[newDoc addWindowController: [[NSWindowController alloc] initWithWindowNibName:[newDoc windowNibName] owner:newDoc]];
[newDoc showWindows];
}
I've also this code inside the openDocument:(id) sender action that does the same but of course loading data to define the application workspace.
If I run the application it show a blank document without to call newDocument action. I don't know how to stop default blank document and to set newDocument: to be called.
Then if i do openDocument: too (so I've two documents, one blank and one not) and I do some operation on the second document it also replicate in the first blank one.
I've double check delegates, file owners, and also if the - (void)windowDidBecomeMain:(NSNotification *)notification return different pointers and all seem to be ok.
Probably I've not understood document based application work flow but I've read the Apple guide and other istructions. What do I miss?
An IBAction method is called, when the user did something. So this is not called from the system at app launch.
You can customize the behavior at app launch with -applicationShouldOpenUntitledFile: (NSApplicationDelegate) and – this is probably your next question – -applicationShouldHandleReopen:hasVisibleWindows: (NSApplicationDelegate). Changing the behavior in both cases is not recommended.
Looking to your action method, I see no reason, why you want to customize it.
A instance of your document class is created automatically.
You can create a window controller for it in your document subclass. This is documented.
Just let NSDocumentController do the work for you. What is the problem of the default behavior?
No. I thought to be confused instead the only problem was about releasing observer notification. When you call the close message for a NSDocument notification observers still persist. Working in ARC I miss this point.
So This is the solution at my issue. Thank you anyway.

Two-way KVO: controller updates model, which notifies controller

I'm currently re-writing a form controller for iOS. It's a custom object that is bound to a model, and handles editing form fields, jumping to the prev/next field, handling custom keyboards, validating data...
The first version was based on a plist for storing the form values, the form controller held all the data itself. Now I want to dissociate the storage (model) from the form controller, thus I've settled with using KVO.
For simplicity's sake, let's assume I've got a form designed to edit a time span for an absence. So it's got two fields: leaveDate and returnDate.
My model is as follows:
#interface Absence
#property (strong, nonatomic) NSDate *leaveDate;
#property (strong, nonatomic) NSDate *returnDate;
#property (readonly, nonatomic) BOOL isValid;
#end
My form controller has a property model which points to this object.
When the user taps on the "leave date" text field in my XIB, the form controller hands in and presents a date picker based on the current value of my model’s leaveDate. When the user picks some other date, the form controller updates its model by using setValue:forKey:.
The isValid property is declared as being impacted by leaveDate and returnDate (using +keyPathsForValuesAffectingIsValid), and the form controller has registered for watching a change in this property, to enable/disable the submit button on the fly.
Up to this point, everything works like a charm. Now, for the twisted part:
I want my form controller to be able to handle changes in the model while it's open. Example: I've got a rule in the model that says "an absence must least at last 3 days". When the users changes the leave date, the return date is automatically adjusted if the total duration does not exceed 3 days.
So my form controller must also register for listening to changes in all properties. The problem is that it both changes the properties, and listens to changes.
That way, when the user changes leaveTime, the form controller uses setValue:forKey: to update the model, but instantly receives a KVO notification for this very change it has just made. This is unnecessary and potentially harmful (I just made the change myself, I don't need to be told I've just done it).
The only way around I found till now is un-registering just before setting the new value, then re-registering right after, like this:
[self.model removeObserver:self forKeyPath:self.currentField.key];
[self.model setValue:newValue forKey:self.currentField.key];
[self.model addObserver:self forKeyPath:self.currentField.key options:NSKeyValueObservingOptionNew context:nil];
It's working, but it's ugly, and performance-wise I doubt it's great.
Does somebody have an explanation as to how to do it better?
TL;DR
ControllerA is a registered KVO observer of Model.
ControllerB updates Model ==> ControllerA receives a KVO notification. That's fine.
ControllerA updates Model ==> ControllerA receives a KVO notification. I don't want this one.
You seem concerned about performance. I wouldn't be. Drawing is coalesced by the main run loop, so setting textField.text = #"foo"; should NOT be causing drawing, image processing, etc. to happen in-line. Typically, a setter like that will set its value and then call [self setNeedsDisplay] which just sets a flag (very cheap), and then later, at the end of the run loop, the drawing system will trigger a single redraw. You could set textField.text a thousand times, and there should still only be one draw operation.
As commenters have suggested, you should make it so your controllers are tolerant of multiple updates. If you're doing a bunch of work in-line with a setter, don't. Setters should be "dumb." They should set the value, and set a flag if necessary (like setNeedsDisplay). In situations like this, you should avoid doing "real work" in a setter.
As another commenter suggested, you could also just not bother updating the UI in-line, and let KVO ripple out the change to all the observers, including the controller that caused the change.
Really, any of these approaches would work, but I suspect that your performance concerns are unfounded. If there is a performance problem, the problem isn't that there are multiple updates, but that you're doing real work during each update, when you should be setting a flag and doing the work later.

CoreData: Clear changes from NSManagedObjectContext

I'm instantiating a NSManagedObjectContext object at the Application delegate level and sharing it across all my UIViewControllers. Here's the code that I use to access it in one of my View Controllers:
NSManagedObjectContext *managedObjectContext = appDelegate.managedObjectContext;
modelObj = (Model *) [NSEntityDescription insertNewObjectForEntityForName:#"Model" inManagedObjectContext:[appDelegate managedObjectContext]];
Now in this screen, I have a UITableView with 9 rows & each cell has a UITextField. As the user inputs values into the textfields, I assign them into modelObj. Now, my user has an option to cancel out and discard all the changes or save them to disk. I have the save code working fine. But in the case when a user tries to discard the changes, I'm not sure what to do. There doesn't seem to be a [managedObjectContext discardChanges] method to throw them all away.
I can think of a couple of ways of solving this.
Create a new instance of NSManagedObjectContext for each controller instead of sharing one across the application.
Or, I could create a bunch of NSStrings in my code and save user values in them and call insertNewObjectForEntityForName: only if the user clicks save.
Which way is the right one? Or is there a way to make NSManagedObjectConext discard all the changes that were made to it?
Thanks,
Teja.
NSManagedObjectContext has a simple method for this:
[managedObjectContext rollback];
This method "removes everything from the undo stack, discards all insertions and deletions, and restores updated objects to their last committed values." (documentation)
Unless I'm missing something, that should give you everything you need.
You might be looking for -refreshObject:mergeChanges: - the docs say that it resets an object from the persistent store, and if you pass NO as the second argument, you can choose not to reapply changes that have been made.
This will likely require you to store a set of objects that you have changed (for the first argument), then clear that set when you commit changes in your context to the store. This should be a pretty trivial addition, though.
Swift 5
managedObjectContext.rollback()

Using NSProgressIndicator inside an NSMenuItem

I'm trying to use a NSProgressIndicator (indeterminate) inside of a statusbar-menu. I'm using an NSView-object as view for the menuitem, and then subviews the progress indicator to display it. But whenever i try to call the startAnimation: for the progress, nothing happens. When i try do the very same thing on a normal NSWindow it works perfectly, just not when inside a menuitem.
I'm new to both cocoa and objective-c so I might've overlooked something "obvious" but I've searched quite a bit for a workaround but without success. I found something about menuitems cant be updated while shown and that you need to use a bordeless window instead. But I have not been able to confirm this in any documentation.
Edit:
Ok, almost works now. When using the setUsesThreadedAnimation: and from a MenuDelegate's menuWillOpen and creating a new thread. This thread runs a local method:
-(void) doWork(NSProgressIndicator*) p{
[p startAnimation:self];
}
This will start the progressindicator on a random(?) basis when opening the menu. If I call startAnimation: directly without going through doWork: (still using a new thread), it never works. Doesn't setUsesThreadedAnimation: make the progress-bar create it's own thread for the animation?
Solved it by using:
[progressIndicator performSelector:#selector(startAnimation:)
withObject:self
afterDelay:0.0
inModes:[NSArray
arrayWithObject:NSEventTrackingRunLoopMode]];
Inside the menuWillOpen:, the problem seems to have been calling startAnimation: before the progressbar was finished drawing itself.
How are you referencing the NSProgressIndicator that is in the view (and the one in the window, for that matter)? For example, do you have a controller class that has IBOutlet's hooked up to the progress indicators? If you are using an IBOutlet, are you sure it's hooked up properly in the nib file?
Also, where and when are you calling startAnimation:? (We need to see some code).
One thing that can sometimes happen is that you forget to hook up an IBOutlet in the nib. Then, when you attempt to tell the object to do something in code at runtime, the IBOutlet is nil, and so what you think is a message being sent to your object is in fact, a message being sent to nil. In other words, it's just ignored, and effectively looks like it's not working.
Provided you do have a (potentially) valid reference to the UI object, the other common issue you'll see is when a developer is trying to send a message to the object at "too early" of a time. In general, init methods are too early in the controller object's lifetime to be able to send messages to user interface objects—those IBOutlet's are still nil. By the time -awakeFromNib is called, IBOutlet's should be valid (provided you hooked them up in IB) and you can then send the message to the UI object.
Have you told it to use threaded animation via -setUsesThreadedAnimation:?

NSTableView -setDataSource not working when triggered by FSEvents

So here's what I've got:
An NSTableView with an NSMutableArray data source
FSEvents monitoring a folder that contains the file that contains the data for the table view (Using SCEvents for Objective-C abstraction goodness)
The FSEvents triggers the same function that a reload button in the UI does. This function refreshes the table view with a new data source based on the contents of said file via setDataSource:.
And here's what happens:
If I make a change to the file, the FSEvent gets triggered and the refresh method gets called.
The array that the table view should be accepting does indeed include the changes that triggered the FSEvent.
setDataSource: gets sent to the NSTableView with the correct data source.
The changes do not appear in the table view!
But then:
If I hit the refresh button, which triggers the exact same method as the FSEvent, the table view gets updated with the new data.
I also tried replacing the FSEvent with an NSNotification (NSApplicationDidBecomeActiveNotification), but the same thing happens.
Anyone have any idea why this is happening?
Edit: For clarification, the jist of my question is this: Why does my NSTableView reload as it should when triggered by a button press, but not when triggered by an FSEvent or an NSNotification?
Edit: Thanks to diciu, I've figured out that in fact all of my UI references point to 0x0 when triggered by the event, but then have valid addresses when triggered by the button click. These objects are all declared in IB, so there's no instantiation or allocation for them going on in my code. So my question is now: what can I do to stop these pointers from pointing to nil?
We call reloadData on NSTableView when we have new data to add/remove to the table.
This might help, to force the NSTableView to redraw.
I'm not really sure if this is what your asking though. The wording of your question is kind of confusing, you state a series of events, but never a true question.
sounds like when you register for the event/notification, you're passing in a different instance of your controller class.
Have you tried calling your method from your FSEvent on a second pass of the run-loop?
[myObject performSelector:#selector(reloadAction:) withObject:nil afterDelay:0.0];
You're setting an NSArray directly as the data source of the table view?
That's not how NSTableView works. The data source must be an object that conforms to NSTableDataSource. NSArray doesn't. You write the data source yourself; it will probably be the same object that you currently have calling setDataSource:.
The other way would be to use Bindings.
Could it be that reference to table view within the scope of your refresh method is not valid?
I.e. are you sure you're not calling [nil reloadData] which does not yield any errors?
Your reference to your table view might be nil in the refresh code if you're set it before awakeFromNib or in some other circumstances.