I'm trying to do an NSAlert using blocks. (I want the last button added to be a cancel button... see below) When I try to call it in my code, I get an error saying an unrecognized selector is being sent to the class. Did I setup my extension improperly?
Here is my call:
[NSAlert showSheetModalForWindow:self.window
WithTitle:#"Allow Sync"
message:message
informativeText:#"Data entered into Easy Spend Log will be shared."
alertStyle:NSWarningAlertStyle
cancelButtonTitle:#"Don't Allow"
otherButtonTitles:#[#"Allow",#"Always Allow"]
onDismiss:^(int buttonIndex) {
if (buttonIndex == 0)
[self.syncEngine allowSync:YES alwaysAllow:NO forConnection:connection];
else
[self.syncEngine allowSync:YES alwaysAllow:YES forConnection:connection];
}
onCancel:^ {
[self.syncEngine allowSync:NO alwaysAllow:NO forConnection:connection];
}];
NSAlert+Blocks.h
#import <Foundation/Foundation.h>
typedef void (^DismissBlock)(int buttonIndex);
typedef void (^CancelBlock)();
#interface NSAlert (Blocks)
+ (NSAlert*) showSheetModalForWindow:(NSWindow*) window
WithTitle:(NSString*) title
message:(NSString*) message
informativeText:(NSString*) text
alertStyle:(NSAlertStyle) style
cancelButtonTitle:(NSString*) cancelButtonTitle
otherButtonTitles:(NSArray*) otherButtons
onDismiss:(DismissBlock) dismissed
onCancel:(CancelBlock) cancelled;
#end
NSAlert+Blocks.m
#import "NSAlert+Blocks.h"
static DismissBlock _dismissBlock;
static CancelBlock _cancelBlock;
#implementation NSAlert (Blocks)
+ (NSAlert*) showSheetModalForWindow:(NSWindow*) window
WithTitle:(NSString*) title
message:(NSString*) message
informativeText:(NSString*) text
alertStyle:(NSAlertStyle) style
cancelButtonTitle:(NSString*) cancelButtonTitle
otherButtonTitles:(NSArray*) otherButtons
onDismiss:(DismissBlock) dismissed
onCancel:(CancelBlock) cancelled {
_cancelBlock = [cancelled copy];
_dismissBlock = [dismissed copy];
NSAlert *alert = [[NSAlert alloc] init];
[alert setMessageText:message];
[alert setInformativeText:text];
[alert setAlertStyle:style];
for(NSString *buttonTitle in otherButtons)
[alert addButtonWithTitle:buttonTitle];
[alert addButtonWithTitle:cancelButtonTitle];
[alert beginSheetModalForWindow:window modalDelegate:self didEndSelector:#selector(alertDidEnd:returnCode:contextInfo:)];
return alert;
}
+ (void)alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo {
if(returnCode == [alert [buttons count]-1])
{
_cancelBlock();
}
else
{
_dismissBlock(returnCode - 1); // cancel button is button 0
}
}
#end
Once I setup a test project, the problem became evident. The call to the alert in the category .m file was wrong.
I have posted the working code onto github. https://github.com/AaronBratcher/NSAlert-Blocks
Related
I want to transition all my old usages of -beginSheetModalForWindow:modalDelegate:didEndSelector:contextInfo: to the recommended -beginSheetModalForWindow:completionHandler:. How do I define contentInfo: and get it in the completionhandler?
Here is an example of how the old code looks like:
[alert beginSheetModalForWindow:window
modalDelegate:self
didEndSelector:#selector(alertDidEnd:returnCode:contextInfo:)
contextInfo:(void *)CFBridgingRetain(fc)];
The endSelector method looks like this:
- (void)alertDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)contextInfo
{
if (returnCode == NSAlertDefaultReturn)
{
FileController *fc = (__bridge FileController *)(contextInfo);
[...]
}
}
}
I guess the new method should look somewhat like this:
[alert beginSheetModalForWindow:window completionHandler:^(NSModalResponse alertReturnCode)
{
if (alertReturnCode == NSAlertFirstButtonReturn)
{
// evaluate contextInfo here ...
}
}];
But I have no clue how to get the contextInfo into the completionhandler.
Any help is appreciated.
There is no context info because the completion handler block can simply look right at the surrounding environment.
NSString* s = #"heyho";
[alert beginSheetModalForWindow:window completionHandler:^(NSModalResponse alertReturnCode) {
if (alertReturnCode == NSAlertFirstButtonReturn)
{
// s is visible here
}
}];
In other words, we don't need to pass a context because we are in a context. If you have a FileController to pass down into the block, just let it pass down into the block.
NSDocument continues to be a software maintenance nightmare.
Anyone else having a problem where they want certain blocking dialogs to be handled SYNCHRONOUSLY?
BEGIN EDIT: I may have found a solution that allows me to wait synchronously
Can anyone verify that this would be an "Apple approved" solution?
static BOOL sWaitingForDidSaveModally = NO;
BOOL gWaitingForDidSaveCallback = NO; // NSDocument dialog calls didSave: when done
...
gWaitingForDidSaveCallback = true;
[toDocument saveDocumentWithDelegate:self
didSaveSelector:#selector(document:didSave:contextInfo:)
contextInfo:nil];
if ( gWaitingForDidSaveCallback )
{
// first, dispatch any other potential alerts synchronously
while ( gWaitingForDidSaveCallback && [NSApp modalWindow] )
[NSApp runModalForWindow: [NSApp modalWindow]];
if ( gWaitingForDidSaveCallback )
{
sWaitingForDidSaveModally = YES;
[NSApp runModalForWindow: [NSApp mbWindow]]; // mbWindow is our big (singleton) window
sWaitingForDidSaveModally = NO;
}
}
...
- (void)document:(NSDocument *)doc didSave:(BOOL)didSave contextInfo:(void *)contextInfo
{
[self recordLastSaveURL];
gWaitingForDidSaveCallback = NO;
if ( sWaitingForDidSaveModally )
[NSApp stopModal];
}
END EDIT
I have to support Snow Leopard/Lion/ML
App termination is an ugly process.
When the user decides to quit, and the document has changes that need saving, I call this:
gWaitingForDidSaveCallback = true;
[toDocument saveDocumentWithDelegate:self
didSaveSelector:#selector(document:didSave:contextInfo:)
contextInfo:nil];
I really really really want this call to be synchronous, but in latest Lion, this hangs my app:
while ( gWaitingForDidSaveCallback )
{
// didSave: callback clears sWaitingForDidSaveCallback
// do my own synchronous wait for now
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceReferenceDate:0.05]];
}
My best guess for the hang is that the mouseDown: of a window close button
is confusing the NSDocument.
So now, I have to return, and pepper my apps main loop with unmaintainable state machine logic to prevent user from executing various dangerous hotkeys.
Ok, so I grin and bear it, and run into yet another roadblock!
In previous OS versions/SDKs, [NSApp modalWindow] would return a window when it
was in this state. Now it doesn't! Grrrrr...
NSDocument has no API to test when it is in this state!
So, now there is no mechanism to globally check this state!
I have to add yet another state variable to my state machine.
Anyone have a cleaner solution for this problem that works in all OS versions and all present (and future) SDKs?
The better way is to save unsaved documents in chain. It is very easy:
// Catch application terminate event
-(NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
{
NSDocumentController *dc = [NSDocumentController sharedDocumentController];
for (NSInteger i = 0; i < [[dc documents] count]; i++)
{
Document *doc = [[dc documents] objectAtIndex:i];
if ([doc isDocumentEdited])
{
// Save first unsaved document
[doc saveDocumentWithDelegate:self
didSaveSelector:#selector(document:didSave:contextInfo:)
contextInfo:(__bridge void *)([NSNumber numberWithInteger:i + 1])]; // Next document
return NSTerminateLater; // Wait until last document in chain will be saved
}
}
return NSTerminateNow; // All documents are saved or there are no open documents. Terminate.
}
...
// Document saving finished
-(void)document:(NSDocument *)doc didSave:(BOOL)didSave contextInfo:(void *)contextInfo
{
if (didSave) // Save button pressed
{
NSDocumentController *dc = [NSDocumentController sharedDocumentController];
NSInteger nextIndex = [(__bridge NSNumber *)contextInfo integerValue];
for (NSInteger i = nextIndex; i < [[dc documents] count]; i++)
{
Document *doc = [[dc documents] objectAtIndex:nextIndex];
if ([doc isDocumentEdited])
{
// Save next unsaved document
[doc saveDocumentWithDelegate:self
didSaveSelector:#selector(document:didSave:contextInfo:)
contextInfo:(__bridge void *)([NSNumber numberWithInteger:nextIndex + 1])]; // Next document
return;
}
}
[NSApp replyToApplicationShouldTerminate:YES]; // All documents saved. Terminate.
}
else [NSApp replyToApplicationShouldTerminate:NO]; // Saving canceled. Terminate canceled.
}
Maybe this answer is too late to be useful but... In one of my apps I implemented -(IBAction)terminate:(id)sender in my NSApplication derived class which would conditionally call [super terminate] to actually close the application only if all open documents were cleanly saved. I may have found some of this in the Apple docs or other examples.
The terminate override will go through each document and either close it (because it's saved), or call the document's canCloseDocumentWithDelegate method in the NSDocument derived class passing 'self' and 'terminate' as the didSaveSelector. Since the terminate method falls through and does nothing except make the document present an NSAlert, the alert in the document class will callback and re-run the terminate routine if the user clicks YES or NO. If all documents are clean, the app will terminate since [super terminate] will get called. If any more dirty documents exist, the process repeats.
For example:
#interface MyApplication : NSApplication
#end
#implementation MyApplication
- (IBAction)terminate:(id)sender
{
//Loop through and find any unsaved document to warn the user about.
//Close any saved documents along the way.
NSDocument *docWarn = NULL;
NSArray *documents = [[NSDocumentController sharedDocumentController] documents];
for(int i = 0; i < [documents count]; i++)
{
NSDocument *doc = [documents objectAtIndex:i];
if([doc isDocumentEdited])
{
if(docWarn == NULL || [[doc windowForSheet] isKeyWindow])
docWarn = doc;
}
else
{
//close any document that doesn't need saving. this will
//also close anything that was dirty that the user answered
//NO to on the previous call to this routine which triggered
//a save prompt.
[doc close];
}
}
if(docWarn != NULL)
{
[[docWarn windowForSheet] orderFront:self];
[[docWarn windowForSheet] becomeFirstResponder];
[docWarn canCloseDocumentWithDelegate:self shouldCloseSelector:#selector(terminate:) contextInfo:NULL];
}
else
{
[super terminate:sender];
}
}
#end
Later in the document derived class:
typedef struct {
void * delegate;
SEL shouldCloseSelector;
void *contextInfo;
} CanCloseAlertContext;
#interface MyDocument : NSDocument
#end
#implementation MyDocument
- (void)canCloseDocumentWithDelegate:(id)inDelegate shouldCloseSelector:(SEL)inShouldCloseSelector contextInfo:(void *)inContextInfo
{
// This method may or may not have to actually present the alert sheet.
if (![self isDocumentEdited])
{
// There's nothing to do. Tell the delegate to continue with the close.
if (inShouldCloseSelector)
{
void (*callback)(id, SEL, NSDocument *, BOOL, void *) = (void (*)(id, SEL, NSDocument *, BOOL, void *))objc_msgSend;
(callback)(inDelegate, inShouldCloseSelector, self, YES, inContextInfo);
}
}
else
{
NSWindow *documentWindow = [self windowForSheet];
// Create a record of the context in which the panel is being
// shown, so we can finish up when it's dismissed.
CanCloseAlertContext *closeAlertContext = malloc(sizeof(CanCloseAlertContext));
closeAlertContext->delegate = (__bridge void *)inDelegate;
closeAlertContext->shouldCloseSelector = inShouldCloseSelector;
closeAlertContext->contextInfo = inContextInfo;
// Present a "save changes?" alert as a document-modal sheet.
[documentWindow makeKeyAndOrderFront:nil];
NSBeginAlertSheet(#"Would you like to save your changes?", #"Yes", #"Cancel", #"No", documentWindow, self,
#selector(canCloseAlertSheet:didEndAndReturn:withContextInfo:), NULL, closeAlertContext, #"%");
}
}
- (void)canCloseAlertSheet:(NSWindow *)inAlertSheet didEndAndReturn:(int)inReturnCode withContextInfo:(void *)inContextInfo
{
CanCloseAlertContext *canCloseAlertContext = inContextInfo;
void (*callback)(id, SEL, NSDocument *, BOOL, void* ) = (void (*)(id, SEL, NSDocument *, BOOL, void* ))objc_msgSend;
if (inAlertSheet) [inAlertSheet orderOut:self];
// The user's dismissed our "save changes?" alert sheet. What happens next depends on how the dismissal was done.
if (inReturnCode==NSAlertAlternateReturn)
{
//Cancel - do nothing.
}
else if (inReturnCode==NSAlertDefaultReturn)
{
//Yes - save the current document
[self saveDocumentWithDelegate:(__bridge id)canCloseAlertContext->delegate
didSaveSelector:canCloseAlertContext->shouldCloseSelector contextInfo:canCloseAlertContext->contextInfo];
}
else
{
// No - just clear the dirty flag and post a message to
// re-call the shouldCloseSelector. This should be
// the app:terminate routine.
[self clearDirtyFlag];
if (canCloseAlertContext->shouldCloseSelector)
{
(callback)((__bridge id)canCloseAlertContext->delegate,
canCloseAlertContext->shouldCloseSelector, self, YES, canCloseAlertContext->contextInfo);
}
}
// Free up the memory that was allocated in -canCloseDocumentWithDelegate:shouldCloseSelector:contextInfo:.
free(canCloseAlertContext);
}
#end
And that should do it - No loops... no waiting...
I have an application that, in part, loops through the contents of an NSSet and displays a UIAlertView for each item found in the set. When there is only one item in the set, the UIAlertView behaves itself properly. However, if there's more than one, the first view flashes up (normally with the contents of the last item in the set) and then disappears without any user intervention. The first item in the NSSet will then display and wait for a response, before displaying the next item in the NSSet and so on.
It is the same experience as is being described in this unresolved question: IPHONE: UIAlertView called twice in a custom function/IBAction
Here's the code:
#import "CalcViewController.h"
#interface CalcViewController()
#property (nonatomic) int variablesCount;
#property (nonatomic, strong) NSMutableDictionary *variablesSet;
#end
#implementation CalcViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.variablesSet = [[NSMutableDictionary alloc] init];
}
- (IBAction)variablePressed:(UIButton *)sender
{
[[self calcModel] setVariableAsOperand:sender.titleLabel.text];
self.expressionDisplay.text = [[self calcModel] descriptionOfExpression:self.calcModel.expression];
}
- (IBAction)solveExpressionPressed:(UIButton *)sender {
self.variablesCount = 0;
[self.variablesSet removeAllObjects];
NSSet *variablesCurrentlyInExpression = [[NSSet alloc] initWithSet:[CalcModel variablesInExpression:self.calcModel.expression]];
self.variablesCount = [variablesCurrentlyInExpression count];
if (variablesCurrentlyInExpression){
for (NSString *item in variablesCurrentlyInExpression) {
UIAlertView *alertDialog;
alertDialog = [[UIAlertView alloc] initWithTitle:#"Enter value for variable"
message:item
delegate:self
cancelButtonTitle:#"OK"
otherButtonTitles:nil];
alertDialog.alertViewStyle=UIAlertViewStylePlainTextInput;
UITextField * alertTextField = [alertDialog textFieldAtIndex:0];
alertTextField.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
[alertDialog show];
}
}
}
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
if (buttonIndex == 0){
if ([[alertView textFieldAtIndex:0] text]){
self.variablesSet[alertView.message] = [[alertView textFieldAtIndex:0] text];
}
}
if ([self.variablesSet count] == self.variablesCount){
NSLog(#"time to solve");
[[self calcDisplay] setText:[NSString stringWithFormat:#"%g", [CalcModel evaluateExpression:self.calcModel.expression usingVariableValues:self.variablesSet]]];
}
}
I've checked the IBActions behind the button that triggers the solveExpressionPressed method and that is the only one that exists. I've also placed some logging before the [alertDialog show]; line and it is only called twice when the variablesCurrentlyInExpression NSSet contains two values, yet the UIAlertView appears three times (flashing up once).
Finally, i've tried it without the following code:
UITextField * alertTextField = [alertDialog textFieldAtIndex:0];
alertTextField.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
and the problem still occurs, so I don't think it's that.
I've been stuck on this a while and haven't figured it out (hence the post!!), so any help would be greatly appreciated.
Thanks
Try showing the first UIAlertView and then showing the second after the first is dismissed.
What's happens is if an app or the OS calls [alert show] and a UIAlertView is already being displayed, the original alertView gets placed in a queue and the new one is presented. When the new one is dismissed, the original UIAlertView is re-shown.
Hope this helps
Easily fixed with a boolean flag that you set to YES when the first alert is shown. Then when the second match is found and the boolean is already YES because the alert is visible you won't show it. Then again you might want to know the exact amount of matches in the NSSet. In that case you keep track with a counter and show the alert after the match function has been done and the counter is not 0.
Avoid showing the alert inside the method of the button trigger. Instead split every function into different sets of methods. Not just for making your function work but maintainability of the code later.
To get this done, you'll need to keep some extra state in your class, like this...
#property (strong, nonatomic) NSMutableSet *promptVariables;
#property (strong, nonatomic) NSString *promptVariable;
#property (strong, nonatomic) NSMutableDictionary *promptResults;
You can probably get away with less by keeping some in your model as it is (or hiding a little in the alert view message as you cleverly do currently), but I'll use all new variables for clarity.
When you want to make several prompts, set up your state like this...
self.promptVariables = [[NSSet alloc] initWithSet:[CalcModel variablesInExpression:self.calcModel.expression]];
[self promptForVariables];
Define promptForVariables to bail if it has no work to do (promptVariables is empty) or remove one and do the alert for it.
- (void)promptForVariables {
if (![self.promptVariables count]) return;
self.promptResults = [NSMutableDictionary dictionary];
self.promptVariable = [self.promptVariables anyObject];
[self.promptVariables removeObject:self.promptVariable];
// do your alert here, I won't repeat your code
}
Then when the alert is done, process the result as you have and call promptForVariables again. The next time, since you've changed state, it has less work to do.
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
if (buttonIndex == 0){
if ([[alertView textFieldAtIndex:0] text]){
[self.promptResults setValue:[[alertView textFieldAtIndex:0] text] forKey:self.promptVariable];
}
[self performSelector:#selector(promptForVariables) withObject:nil afterDelay:0.0];
}
}
When this is done, promptResults will contain variable names as keys and user inputs as values.
According to the official FAQ from ver.2 to customize your text/content depending on what sharer was selected by the user, you need:
subclass from SHKActionSheet and override
dismissWithClickedButtonIndex
set your new subclass name in
configurator (return it in (Class)SHKActionSheetSubclass;).
It doesn't work for me. But even more: I put
NSLog(#"%#", NSStringFromSelector(_cmd));
in (Class)SHKActionSheetSubclass to see if it's even got called. And it's NOT ;(( So ShareKit doesn't care about this config option...
Has anybody worked with this before?
thank you!
UPD1: I put some code here.
Here's how my subclass ITPShareKitActionSheet looks like. According to the docs I need to override dismissWithClickedButtonIndex:animated:, but to track if my class gets called I also override the actionSheetForItem::
+ (ITPShareKitActionSheet *)actionSheetForItem:(SHKItem *)item
{
NSLog(#"%#", NSStringFromSelector(_cmd));
ITPShareKitActionSheet *as = (ITPShareKitActionSheet *)[super actionSheetForItem:item];
return as;
}
- (void)dismissWithClickedButtonIndex:(NSInteger)buttonIndex animated:(BOOL)animate
{
NSLog(#"%#", NSStringFromSelector(_cmd));
NSString *sharersName = [self buttonTitleAtIndex:buttonIndex];
[self changeItemForService:sharersName];
[super dismissWithClickedButtonIndex:buttonIndex animated:animate];
}
And here's what I do in code to create an action sheet when user presses 'Share' button:
- (IBAction)shareButtonPressed:(id)sender
{
// Create the item to share
SHKItem *item = [SHKItem text:#"test share text"];
// Get the ShareKit action sheet
ITPShareKitActionSheet *actionSheet = [ITPShareKitActionSheet actionSheetForItem:item];
// Display the action sheet
[actionSheet showInView:self.view]; // showFromToolbar:self.navigationController.toolbar];
}
When I run this code, press 'Share' button and select any sharer I expect to get two lines in log:
actionSheetForItem: - custom action sheet got created
dismissWithClickedButtonIndex:animated: - custom mechanics to
process action sheet's pressed button got called.
But for some reason I get only the first line logged.
I was having the same issues but I've suddenly got it to call my Subclass successfully.
Firstly My Configurator is setup as so:
-(Class) SHKActionSheetSubclass{
return NSClassFromString(#"TBRSHKActionSheet");
}
Now My Subclass:
.h File
#import "SHKActionSheet.h"
#interface TBRSHKActionSheet : SHKActionSheet
#end
.m implementation override:
#import "TBRSHKActionSheet.h"
#import "SHKActionSheet.h"
#import "SHKShareMenu.h"
#import "SHK.h"
#import "SHKConfiguration.h"
#import "SHKSharer.h"
#import "SHKShareItemDelegate.h"
#implementation TBRSHKActionSheet
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
}
return self;
}
+ (SHKActionSheet *)actionSheetForItem:(SHKItem *)i
{
NSLog(#"%#", NSStringFromSelector(_cmd));
SHKActionSheet *as = [self actionSheetForType:i.shareType];
as.item = i;
return as;
}
- (void)dismissWithClickedButtonIndex:(NSInteger)buttonIndex animated:(BOOL)animated
{
NSInteger numberOfSharers = (NSInteger) [sharers count];
// Sharers
if (buttonIndex >= 0 && buttonIndex < numberOfSharers)
{
bool doShare = YES;
SHKSharer* sharer = [[NSClassFromString([sharers objectAtIndex:buttonIndex]) alloc] init];
[sharer loadItem:item];
if (shareDelegate != nil && [shareDelegate respondsToSelector:#selector(aboutToShareItem:withSharer:)])
{
doShare = [shareDelegate aboutToShareItem:item withSharer:sharer];
}
if(doShare)
[sharer share];
}
// More
else if ([SHKCONFIG(showActionSheetMoreButton) boolValue] && buttonIndex == numberOfSharers)
{
SHKShareMenu *shareMenu = [[SHKCONFIG(SHKShareMenuSubclass) alloc] initWithStyle:UITableViewStyleGrouped];
shareMenu.shareDelegate = shareDelegate;
shareMenu.item = item;
[[SHK currentHelper] showViewController:shareMenu];
}
[super dismissWithClickedButtonIndex:buttonIndex animated:animated];
}
Finally on my implementation file I've not modified the call to SHKActionSheet as Vilem has suggested because of some dependancies that seemed to cause conflicts for me.
So this is my caller (straight from tutorial):
NSURL *url = [NSURL URLWithString:#"http://getsharekit.com"];
SHKItem *item = [SHKItem URL:url title:#"ShareKit is Awesome!" contentType:SHKURLContentTypeWebpage];
// Get the ShareKit action sheet
SHKActionSheet *actionSheet = [SHKActionSheet actionSheetForItem:item];
// ShareKit detects top view controller (the one intended to present ShareKit UI) automatically,
// but sometimes it may not find one. To be safe, set it explicitly
[SHK setRootViewController:self];
// Display the action sheet
[actionSheet showFromToolbar:self.navigationController.toolbar];
This Calls no problems for me.
edit: by far the best way to achieve this is to use SHKShareItemDelegate. More info is in ShareKit's FAQ.
How do I display a popup informing a NSTextField can't be empty in cocoa ?
If the user click apply and the NSTextField is empty a popup should appear saying the field can't be empty.
thanks
The answer by #beryllium only tells part of the story.
In fact, to properly validate text field input in Cocoa you should be using an NSFormatter attached to your text field cell and then in your NSTextFieldDelegate you should implement the method: control:didFailToFormatString:errorDescription:. In this delegate method you can prompt the user to correct their input.
All you need to do in your NSFormatter subclass is something like this:
#implementation RKTextFormatter
- (NSString*)stringForObjectValue:(id)object
{
return (NSString*)object;
}
- (BOOL)getObjectValue:(id*)object forString:(NSString*)string errorDescription:(NSString**)error {
BOOL stringPassesTest = NO;
//put your test here
if(!stringPassesTest)
{
//set the error and return NO
if(error)
*error = [NSError errorWithDomain:#"YourErrorDomain" code:1 userInfo:[NSDictionary dictionaryWithObject:#"It's a bingo" forKey:NSLocalizedDescriptionKey]];
return NO;
}
//otherwise, just assign the string
*object = string;
return YES;
}
#end
You would assign the formatter to your text field like so:
RKTextFormatter* formatter = [[RKTextFormatter alloc] init];
[[textField cell] setFormatter:formatter];
And then in your NSTextFieldDelegate you handle any invalid input:
- (BOOL)control:(NSControl *)control didFailToFormatString:(NSString *)string errorDescription:(NSString *)error
{
//display an alert, obviously it would be more useful than this
NSAlert* alert = [NSAlert alertWithMessageText:#"You have failed me for the last time" defaultButton:#"Revise Input" alternateButton:nil otherButton:nil informativeTextWithFormat:#"%#",error];
[alert beginSheetModalForWindow:control.window modalDelegate:nil didEndSelector:NULL contextInfo:NULL];
//if you return NO here, the user's input will not be accepted
//and the field will remain in focus
return NO;
}
If the user click apply and the NSTextField is empty a popup should
appear saying the field can't be empty.
Please, please, do not do this. You can be smarter than that.
Instead of investing your time in writing an alert dialog to handle the "unexpected" situation, invest it in creating a method that prevents the problem from occurring in the first place: keep the Apply button disabled until a proper value has been entered in the text field.
In addition, as #Rob Keniger mentioned, you should consider using NSFormatters to validate the input to make sure it's of the appropriate kind.
Try to use this code:
- (IBAction)pushBtn:(id)sender {
if(self.textfield.stringValue.length == 0){
NSAlert * alert = [NSAlert alertWithMessageText:#"Error"
defaultButton:#"OK"
alternateButton:nil
otherButton:nil
informativeTextWithFormat:#"Enter a text into text field"];
[alert beginSheetModalForWindow:self.window
modalDelegate:self
didEndSelector:#selector(alertDidHidden:)
contextInfo:nil];
//[alert runModal]; - more simple way
}
}
-(void)alertDidHidden:(NSAlert *)alert{
}