NSTextField with NSFormatter results in broken continuous binding - macos

I have a textfield which has to be unique so I added my custom NSFormatter (see below)
The formatter works, as you can see on the screenshot, but the continuous binding, which I am using is broken, so for example the bound text does no longer get updated in real-time.
I found a possible cause here, but I don't know how to work around this problem and re-enable the continuous binding:
...
12. If the view has an NSFormatter attached to it, the value is
formatted by the NSFormatter instance. Proceed to Step 17.
...
17. The updated value is displayed in the user interface.
So it looks like it's intentionally skipping the steps we want. This
is very annoying. I tried NSValueTransformer, but adding that to an
editable NSTextField makes it non-editable.
My formatter
- (BOOL)getObjectValue:(out id *)obj forString:(NSString *)string errorDescription:(out NSString **)error {
if([string isNotEqualTo:#"todo-invalid-value"]){
*obj = string;
NSLog(#"YES");
return YES;
} else {
if(error){
*error = #"ERROR: not allowed";
}
return NO;
}
}
- (NSString *)stringForObjectValue:(id)obj {
return (NSString *)obj;
}
Working validation
Please note that the title of the list item should be updated with the text, that I entered in the textfield.

I ran into the same problem over the weekend, and eventually discovered a post from 2008 by Yann Disser on the cocoa-dev mailing list which shed some light on my problem.
I had an existing NSFormatter that was working fine and when I broke down the components, so I spent a little more time on it this morning and located Yann's post.
The key is that you need to return a different object than the one that is passed in. It's subtle, but the docs say: If conversion is successful, upon return contains the object created from string.
The problem I was having stemmed from the fact that the NSString that was coming in was actually an NSMutableString and was getting modified later.
Here's the code modified to return [NSString stringWithString: string], which should fix your problem:
- (BOOL)getObjectValue:(out id *)obj forString:(NSString *)string errorDescription:(out NSString **)error {
if([string isNotEqualTo:#"todo-invalid-value"]){
*obj = [NSString stringWithString: string];
NSLog(#"YES");
return YES;
} else {
if(error){
*error = #"ERROR: not allowed";
}
return NO;
}
}

Related

Mixing tokens and strings in NSTokenField

I want to have an NSTokenField that contains both plain text and tokens. That's the same problem as in this question, but the answers there haven't solved it for me. Maybe I'm missing something, or maybe Apple changed something in the 5 years since those answers were posted.
Specifically, let's say I want to type "hello%tok%" and have it turn into this:
In order to try to remove chances for confusion, I always use a custom represented object, of one of the following classes, rather than a plain string...
#interface Token : NSObject
#end
#implementation Token
#end
#interface WrappedString : NSObject
#property (retain) NSString* text;
#end
#implementation WrappedString
#end
Here are my delegate methods:
- (NSString *)tokenField:(NSTokenField *)tokenField
displayStringForRepresentedObject:(id)representedObject
{
NSString * displayString = nil;
if ([representedObject isKindOfClass: [WrappedString class]])
{
displayString = ((WrappedString*)representedObject).text;
}
else
{
displayString = #"TOKEN";
}
return displayString;
}
- (NSTokenStyle)tokenField:(NSTokenField *)tokenField
styleForRepresentedObject:(id)representedObject
{
NSTokenStyle theStyle = NSPlainTextTokenStyle;
if ([representedObject isKindOfClass: [Token class]])
{
theStyle = NSRoundedTokenStyle;
}
return theStyle;
}
- (NSString *)tokenField:(NSTokenField *)tokenField
editingStringForRepresentedObject:(id)representedObject
{
NSString * editingString = representedObject;
if ([representedObject isKindOfClass: [Token class]])
{
editingString = nil;
}
else
{
editingString = ((WrappedString*)representedObject).text;
}
return editingString;
}
- (id)tokenField:(NSTokenField *)tokenField
representedObjectForEditingString:(NSString *)editingString
{
id repOb = nil;
if ([editingString isEqualToString:#"tok"])
{
repOb = [[[Token alloc] init] autorelease];
}
else
{
WrappedString* wrapped = [[[WrappedString alloc]
init] autorelease];
wrapped.text = editingString;
repOb = wrapped;
}
return repOb;
}
As I'm typing the "hello", none of the delegate methods is called, which seems reasonable. When I type the first "%", there are 3 delegate calls:
tokenField:representedObjectForEditingString: gets the string "hello" and turns it into a WrappedString representation.
tokenField:styleForRepresentedObject: gets that WrappedString and returns NSPlainTextTokenStyle.
tokenField:editingStringForRepresentedObject: gets the WrappedString and returns "hello".
The first two calls seem reasonable. I'm not sure about number 3, because the token should be editable but it's not being edited yet. I would have thought that tokenField:displayStringForRepresentedObject: would get called, but it doesn't.
When I type "tok", no delegate methods are called. When I type the second "%", tokenField:representedObjectForEditingString: receives the string "hellotok", where I would have expected to see just "tok". So I never get a chance to create the rounded token.
If I type the text in the other order, "%tok%hello", then I do get the expected result, a round token followed by plain "hello".
By the way, the Token Field Programming Guide says
Note that there can be only one token per token field that is configured for the plain-text token style.
which seems to imply that it's not possible to freely mix plain text and tokens.
I asked myself whether I had seen mixed text and tokens anywhere in standard apps, and I had. In the Language & Text panel of System Preferences, under the Formats tab, clicking one of the "Customize..." buttons brings up a dialog containing token fields. Here's part of one.
Here, you don't create tokens by typing a tokenizing character, you drag and drop prototype tokens.
To make one of the prototype tokens, make another NSTokenField and set it to have no background or border and be selectable but not editable. When your window has loaded, you can initialize the prototype field using the objectValue property, e.g.,
self.protoToken.objectValue = #[[[[Token alloc] init] autorelease]];
You need to set up a delegate for each prototype token field as well as your editable token field. In order to be able to drag and drop tokens, your delegate must implement tokenField:writeRepresentedObjects:toPasteboard: and tokenField:readFromPasteboard:.

adding a Core Data object from a segue

in getting familiar with core data i have found myself puzzled by the question of what to pass various view controllers (VCs) when trying to add data.
for example, in the CoreDataRecipes project that apple provides as an example (http://developer.apple.com/library/ios/#samplecode/iPhoneCoreDataRecipes/Introduction/Intro.html) they use the following approach
when the user wants to add a recipe to the list of recipes presented in the master table view, and hits the Add button, the master table view controller (called RecipeListTableViewController) creates a new managed object (Recipe) as follows:
- (void)add:(id)sender {
// To add a new recipe, create a RecipeAddViewController. Present it as a modal view so that the user's focus is on the task of adding the recipe; wrap the controller in a navigation controller to provide a navigation bar for the Done and Save buttons (added by the RecipeAddViewController in its viewDidLoad method).
RecipeAddViewController *addController = [[RecipeAddViewController alloc] initWithNibName:#"RecipeAddView" bundle:nil];
addController.delegate = self;
Recipe *newRecipe = [NSEntityDescription insertNewObjectForEntityForName:#"Recipe" inManagedObjectContext:self.managedObjectContext];
addController.recipe = newRecipe;
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:addController];
[self presentModalViewController:navigationController animated:YES];
[navigationController release];
[addController release];
}
this newly created object (a Recipe) is passed to the RecipeAddViewController. the RecipeAddViewController has two methods, save and cancel, as follows:
- (void)save {
recipe.name = nameTextField.text;
NSError *error = nil;
if (![recipe.managedObjectContext save:&error]) {
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
[self.delegate recipeAddViewController:self didAddRecipe:recipe];
}
- (void)cancel {
[recipe.managedObjectContext deleteObject:recipe];
NSError *error = nil;
if (![recipe.managedObjectContext save:&error]) {
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
[self.delegate recipeAddViewController:self didAddRecipe:nil];
}
i am puzzled about this design approach. why should the RecipeListViewController create the object before we know if the user wants to actually enter a new recipe name and save the new object? why not pass the managedObjectContext to the addRecipeController, and wait until the user hits save to create the object and populate its fields with data? this avoids having to delete the new object if there is no new recipe to add after all. or why not just pass a recipe name (a string) back and forth between the RecipeListViewController and the RecipeAddController?
i'm asking because i am struggling to understand when to pass strings between segues, when to pass objects, and when to pass managedObjectContexts...
any guidance much appreciated, incl. any links to a discussion of the design philosophies at issue.
Your problem is that NSManagedObjects can't live without a context. So if you don't add a Recipe to a context you have to save all attributes of that recipe in "regular" instance variables. And when the user taps save you create a Recipe out of these instance variables.
This is not a huge problem for an AddViewController, but what viewController do you want to use to edit a recipe? You can probably reuse your AddViewController. But if you save all data as instance variables it gets a bit ugly because first you have to get all data from the Recipe, save it to instance variables, and when you are done you have to do the reverse.
That's why I usually use a different approach. I use an editing context for editing (or adding, which is basically just editing).
- (void)presentRecipeEditorForRecipe:(MBRecipe *)recipe {
NSManagedObjectContext *editingContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
editingContext.parentContext = self.managedObjectContext;
MBRecipe *recipeForEditing;
if (recipe) {
// get same recipe inside of the editing context.
recipeForEditing = (MBRecipe *)[editingContext objectWithID:[recipe objectID]];
NSParameterAssert(recipeForEditing);
}
else {
// no recipe for editing. create new one
recipeForEditing = [MBRecipe insertInManagedObjectContext:editingContext];
}
// present editing view controller and set recipeForEditing and delegate
}
Pretty straight forward code. It creates a new children context which is used for editing. And gets a recipe for editing from that context.
You must not save the context in your EditViewController! Just set all desired attributes of Recipe, but leave the context alone.
After the user tapped "Cancel" or "Done" this delegate method is called. Which either saves the editingContext and our context or does nothing.
- (void)recipeEditViewController:(MBRecipeEditViewController *)editViewController didFinishWithSave:(BOOL)didSave {
NSManagedObjectContext *editingContext = editViewController.managedObjectContext;
if (didSave) {
NSError *error;
// save editingContext. this will put the changes into self.managedObjectContext
if (![editingContext save:&error]) {
NSLog(#"Couldn't save editing context %#", error);
abort();
}
// save again to save changes to disk
if (![self.managedObjectContext save:&error]) {
NSLog(#"Couldn't save parent context %#", error);
abort();
}
}
else {
// do nothing. the changes will disappear when the editingContext gets deallocated
}
[self dismissViewControllerAnimated:YES completion:nil];
// reload your UI in `viewWillAppear:`
}

NSKeyedArchiver: distinguishing between different instances of the same class

I'm implementing support for Lion's "Resume" feature in my OS X app.
I have a custom subclass of NSViewController in which I implemented the method
encodeRestorableStateWithCoder: as:
#implementation MyClass (Restoration)
-(void)encodeRestorableStateWithCoder:(NSCoder*)coder {
[coder encodeObject:_dataMember forKey:#"object_key"]; // I get the warning below when this line is executed for the second time
}
- (void)restoreStateWithCoder:(NSCoder *)coder {
_dataMember = [coder decodeObjectForKey:#"object_key"];
}
#end
However, since I have multiple instances of MyClass, different values are saved into the same key ("object_key") and I get the following warning from Cocoa:
NSKeyedArchiver warning: replacing existing value for key
'object_key'; probable duplication of encoding keys in class hierarchy
What is the best practice to overcome this problem?
Edit: I found here that each instance automatically has its own namespace to avoid collisions, so the problem might be in the way I'm manually calling encodeRestorableStateWithCoder to different instances with the same NSCoder object without telling it that these are different instances. However, I still can't figure out how to do that properly.
Thanks in advance!
To overcome this problem, it is possible to create a new NSMutableData where each of which is written by a separate (new) NSKeyArchiver, and store them all in an array which is stored in the original NSCoder object.
Here is an example for encoding the restorable state of subitems. The decoding part can be straight-forward given this code.
- (void)encodeRestorableStateWithCoder:(NSCoder *)coder
{
[super encodeRestorableStateWithCoder:coder];
// Encode subitems states:
NSArray* subitems = self.items;
NSMutableArray* states = [NSMutableArray arrayWithCapacity: subitems.count];
for (SubItemClass* item in subitems)
{
NSMutableData* state = [NSMutableData data];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:state];
[item encodeRestorableStateWithCoder:archiver];
[archiver finishEncoding];
[states addObject:state];
}
[coder encodeObject:states forKey:#"subitems"];
}

Get variable from void function in Objective C

I'm VERY new to Objective C and iOS development (like 5 hours new :-). I've got some code that calls an API to authenticate a user and returns a simple OK or FAIL. I can get the result to write to the console but what I need to do is get that result as part of my IBAction.
Here's the IBAction code:
- (IBAction) authenticateUser
{
[txtEmail resignFirstResponder];
[txtPassword resignFirstResponder];
[self performAuthentication];
if (authResult == #"OK")
What I need is for authResult to be the JSON result (OK or FAIL). Here is the code that gets the result:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
[connection release];
NSString *responseString = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
NSLog(#"%#", responseString);
[responseData release];
NSMutableDictionary *jsonResult = [responseString JSONValue];
if (jsonResult != nil)
{
NSString *jsonResponse = [jsonResult objectForKey:#"Result"];
NSLog(#"%#", jsonResponse);
}
}
Thank you so much for any help and sorry if I'm missing something obvious!
I'm a little confused as to what's going on here... it looks like your -performAuthentication method must start an asynchronous network request via NSURLConnection, and your connection's delegate's -connectionDidFinishLoading: gets to determine the result of the request. So good so far? But your -authenticateUser method expects authResult to be determined as soon as -performAuthentication returns. If the network request is asynchronous, that's not going to happen. If I'm following you, I think you need to do the following:
Fix up -connectionDidFinishLoading: so that it actually sets authResult based on the Result value in jsonResponse. I'm sure you'd get around to this at some point anyway.
Change -authenticateUser such that it doesn't expect to have an answer immediately. You've got to give the network request a chance to do its thing.
Add another method, possibly called -authenticationDidFinish or something along those lines. Everything currently in -authenticateUser from the 'if (authResult...' to the end goes in this new method.
Call the new method from -connectionDidFinishLoading:.
Fix your string comparison. If you want to compare two strings in Cocoa, you say (for example):
if ([authResult isEqualToString:#"OK") { }

Simple NSSpeechRecognizer code, not working!

I noticed NSSpeechRecognizer in ADC library and I found it to be very interesting, so to play with it I prepared a simple application which will just listen the command and if recognized it displays it in log.
The code used is:
- (id)init {
if (self = [super init]) {
// Insert code here to initialize your application
NSArray *cmds = [NSArray arrayWithObjects:#"A",#"B", #"C",#"alpha",#"beta",#"vodka",#"wine",nil];
recog = [[NSSpeechRecognizer alloc] init]; // recog is an ivar
[recog setCommands:cmds];
[recog setDelegate:self];
}
return self;
}
- (IBAction)listen:(id)sender
{ NSLog(#"listen:");
if ([sender state] == NSOnState) { // listen
[recog startListening];
} else {
[recog stopListening];
}
}
- (void)speechRecognizer:(NSSpeechRecognizer *)sender didRecognizeCommand:(id)aCmd {
NSLog(#"speechRecognizer: %#",(NSString *)aCmd);
}
I tried it many times for the commands registered but I was unable to get none of the messages in log, in delegate :(
There was always some noise in the background.. could this be the reason for it or I have done something wrong in the code??
Can anyone suggest me some solution for it??
Thanks,
Miraaj
Code looks fine so far.
The NSSpeechRecognizer is a bit tricky sometimes and refuses to listen to the right words. Did you try different words?
Did you try setting startListening as default?
I wrote a little tutorial some time ago. Its in german language but maybe it will help you anyway or you use some translation tool.
http://cocoa-coding.de/spracherkennung/nsspeechrecognizer1.html

Resources