NSTextField Autocomplete / Suggestions - objective-c

Since some days I am trying to code an autocompletion for a NSTextField. The autocompletion should look that way, that when the user clicks on the NSTextfield a list should be displayed under the TextField which possibilities are available. After typing one letter or number the list should refresh with the possibilities.
The suggestions in this list should come from an NSMutableArrayor a NSMutableDictionary
This autocomplete / autosuggestion method should be for a MAC application.

Just adding to #AbcdEfg's answer, to make NSComboBox case-insensitive, you can subclass it and override it's [completedString:] method like this:
- (NSString *) completedString:(NSString *)string {
NSUInteger l = [string length];
if (!!l)
for (NSString *match in [self objectValues])
if ([[match commonPrefixWithString:string options:NSCaseInsensitiveSearch] length] == l)
return [match stringByReplacingCharactersInRange:NSMakeRange(0, l) withString:string];
return nil;
}

You can use NSComboBox for that matter. You also need to set its Autocompletes attribute in IB or [comboBox setCompletes:YES] in code.
Keep in mind that it is case-sensitive.
However if you need it to be done in the exact way that you described, you need to make the list by subclassing NSWindowController and NSTableView and change them to look like a list or menu to show under you NSTextField. Set the NSTextField's delegate and do the search and list update on text changes.
Avoid the NSMenu in this case as it will take away the focus from the text field while you are typing.
Apple addressed it in WWDC 2010 Session 145.
They explained about a text field with suggestions menu and how to get it to work. They also provide the sample codes in their website.
You can find the sample code here.

Related

Custom NSTextView insertText:replacementRange breaks Spell Checking

I have a custom NSTextView subclass, with a custom NSTextStorage component as well. The NSTextStorage modifies the text entered by the user based on context.
Because it's possible that the final text will be shorter than the text originally entered by the user, I had to override insertText:replacementRange in my NSTextView. A minimum example is:
- (void) insertText:(id)string replacementRange:(NSRange)replacementRange {
if ([self hasMarkedText]) {
[[self textStorage] replaceCharactersInRange:[self markedRange] withString:string];
} else {
[[self textStorage] replaceCharactersInRange:[self selectedRange] withString:string];
}
[self didChangeText];
}
This works fine in extensive testing over several months.... Except that automatic spell checking and correction is disabled. The "squigglies" don't appear under misspelled words, unless I stop typing, move the mouse, and switch focus to and from my app. After several seconds, the entire textview is spellcheck'ed. Because it happens after the fact, automatic correction is disabled of course.
If I disable my custom insertText:replacementRange: method, everything else works fine, and automatic spelling functionality returns. I just have to be careful not to trigger a change that results in shortening the text, as it triggers attribute out of range errors (the original reason for my custom method in the first place.)
Apparently Apple's implementation of insertText:replacementRange: does much more than mine. I have tried multiple variations on [self checkTextInRange...], [self checkTextInSelection:], etc. None of them restore proper functionality.
Searching Apple's documentation doesn't help point me towards what I am leaving out from my method that is causing spell checking to break. Any pointers or ideas would be much appreciated!!
Thanks in advance!
EDIT: Here are some examples of the sorts of behavior my NSTextStorage provides. (| represents the insertion caret)
Starting with:
* item
* |
If I hit the return key, I end up with the following (deleting *<space>):
* item
|
Another example, if "Change Tracking" is enabled:
this is thee| time
If I hit delete:
this is the|{--e--} time
As you can see, a single keystroke may result in the addition or deletion of multiple characters from the text.
EDIT 2: FYI -- the issue I have with attributes being out of range occur when the shortening happens while pressing return at the end of the document -- NSTextview attempts to set a new paragraph style only to find that the document is shorter than expected. I can find no way to change the range NSTextview targets.
I have a partial solution.
In my custom insertText:replacementRange: method, prior to didChangeText:
NSinteger wordCount;
NSOrthography * orthography;
static NSInteger theWordCount;
NSOrthography * orthography;
NSRange spellingRange = <range to check>
NSArray * results = [[NSSpellChecker sharedSpellChecker] checkString:[[self textStorage] string]
range:spellingRange
types:[self enabledTextCheckingTypes]
options:NULL
inSpellDocumentWithTag:0
orthography:&orthography
wordCount:&theWordCount];
if (results.count) {
[self handleTextCheckingResults:results forRange:spellingRange types:[self enabledTextCheckingTypes] options:#{} orthography:orthography wordCount:theWordCount];
}
However, this is incomplete:
Spell check and Grammar check works fine
Automatic spelling correction and text replacement do not work (even when enabled)
(EDITED 2018-05-30)
Updated response (2018-05-22):
This issue reared its ugly head again, and I really needed to figure it out.
My custom NSTextStorage is fundamentally the same as described, and still works.
I use a custom insertText:replacementRange: on my NSTextView, but it calls [super insertText:replacementRange:] to take advantage of Apple's behind-the-scenes work that makes spelling, etc. work better. My custom method only needs to set a boolean.
When shortening the text, I still get requests from Apple's insertText:replacementRange: for attributes in a non-existent part of the text. Previously, I would get stuck here, because everything I tried either caused a crash, or caused Apple's code to repeatedly request the non-existing attributes indefinitely.
Finally, I tried returning fake attributes with a NULL rangepointer, and this seems to make Apple's code happy:
- (NSDictionary *) attributesAtIndex:(NSUInteger)location effectiveRange:(nullable NSRangePointer)range {
if (location > _backingAttributes.length) {
// This happens if we shrink the text before the textview is aware of it.
// For example, if we expand "longtext" -> "short" in our smart string, then
// The textview may set and request attributes past the end of our
// _backing string.
// Initially this was due to error in my code, but now I had to add
// This error checking back
NSLog(#"get attributes at (%lu) in (%lu)", (unsigned long)location, (unsigned long)_backingAttributes.length);
NSLog(#"error");
// Apparently returning fake attributes satisfies [NSTextView insertText:replacementRange:]
range = NULL;
return #{
NSForegroundColorAttributeName : [BIColor redColor],
NSFontAttributeName : [BIFont fontWithName:#"Helvetica" size:14.0]
};
} else {
return [_backingAttributes attributesAtIndex:location effectiveRange:range];
}
}
With further testing, this turned out to not be quite enough. I ended up adding the following to the setter to store the invalid attributes and range that macOS was trying to set:
- (void) setAttributes:(NSDictionary<NSString *,id> *)attrs range:(NSRange)range {
if (NSMaxRange(range) > _backingAttributes.length) {
_invalidAttrs = attrs;
_invalidRange = range;
} else {
[self beginEditing];
[_backingAttributes setAttributes:attrs range:range];
[self edited:NSTextStorageEditedAttributes range:range changeInLength:0];
[self endEditing];
}
}
I updated `attributesAtIndex:effectiveRange: to return the following when called with an invalid range, rather than returning the fake attributes above:
// Apparently returning fake attributes satisfies [NSTextView insertText]
*range = _invalidRange;
return _invalidAttrs;
This seems to work under various conditions that would previously trigger an exception or an infinite loop.

Iterate over multiple checkboxes and get titles of selected

I have an app with a window containing a box that contains maybe 100 checkboxes.
When I press a button I'd like to build a string of the titles of the selected checkboxes.
I know I need to iterate over all the checkboxes and get the titles but I can't figure out how to do that. Any pointers would be greatly appreciated. I know how to do it with bindings and tags but with 100 checkboxes that's just crazy!
Thanks!
I'd suggest you to use a stack view or a collection view. If you want to use simple checkboxes you have to keep pointers either in an array to iterate over or put them in a custom view as container where you iterate over its subviews.
This example will point you in a direction:
NSMutableArray *activeButtons = [NSMutableArray new];
for (NSView* subview in [self.container subviews])
if ([subview isMemberOfClass:[NSButton class]] && [(NSButton*)subview state] == 1)
[activeButtons addObject:[(NSButton*)subview title]];
NSString* text = [activeButtons componentsJoinedByString:#", "];
NSLog(#"Active: %#",text);
The isMemberOfClass method will ensure to only get buttons and not texts. you may additionaly check for the kind of button to ensure to only get checkboxes if you also have other buttons in the container. There are faster ways like enumeration but this is a better starting point imho.
Note: written on mobile... untested so excuse a possible typo (though double checked)

Writing an app (for my class) which states user input on state names and that particular state's capital.

The app is supposed to work like this:
The user inputs, in a text field, a particular state. In another text field, the user inputs a capital for that state.
After the user hits the "add" button, the key/value pair is sent into both dictionaries, one for pairing State to Capital, and another for comparing Capital to State.
So here's my problem:
All that being said, and the app programmatically creating a button in a scrollView, the State or whichever data was typed into the first field, is represented.
That particular button is supposed to, upon clicking, switch the name of that button to the paired Capital or State, in whichever case. I cannot, for the life of me, find out how to code this into my app.
My Header File:
IBOutlet UIScrollView *scrollView; // for scrollable State / Capital view
IBOutlet UITextField *stateField; // text field for entering state
IBOutlet UITextField *capitalField; // text field for entering capital
// stores the website and passwds
NSMutableDictionary *stateCapital;
NSMutableDictionary *capitalState;
// stores the Buttons representing the passwds
NSMutableArray *buttons;
// stores the info buttons for editing existing passwd
NSMutableArray *label;
// location of the file in which capital are stored
NSString *filePath;
My Implementation:
To add a state
// make the keyboard disappear
[stateField resignFirstResponder];
[capitalField resignFirstResponder];
NSString *key = stateField.text; // get the text in tagField
NSString *value = capitalField.text; // get the text in queryField
// test if either field is empty
if (value.length == 0 || key.length == 0)
return; // exit from the method
if ([stateCapital valueForKey:key] == nil) // test if the website already exists
[self addNewButtonWithTitle:key]; // if not, add a new button
[stateCapital setValue:value forKey:key]; // add a new entry in tags
stateField.text = nil; // clear tagField of text
capitalField.text = nil; // clear queryField of text
[stateCapital writeToFile:filePath atomically:NO]; //save the data
To add a Capital (just reverse the process)
// make the keyboard disappear
[stateField resignFirstResponder];
[capitalField resignFirstResponder];
NSString *key = capitalField.text; // get the text in tagField
NSString *value = stateField.text; // get the text in queryField
// test if either field is empty
if (value.length == 0 || key.length == 0)
return; // exit from the method
if ([capitalState valueForKey:key] == nil) // test if the website already exists
[self addNewButtonWithTitle:key]; // if not, add a new button
[capitalState setValue:value forKey:key]; // add a new entry in tags
stateField.text = nil; // clear tagField of text
capitalField.text = nil; // clear queryField of text
[capitalState writeToFile:filePath atomically:NO]; //save the data
I'm pretty new to programming, and am taking a Summer course, it's extremely compact.
My professor is in and out of the lab, but offers only hints on how to achieve the success of this application. I'm trying to find out via the web and my book on how to do this, but have no success as of yet.
Generally, you want individual IBOutlets for each UI widget that's part of your interaction. (I assume you're using Storyboard…) You have outlets for UITextFields, but your buttons are in an NSMutableArray. This is almost surely not what you want. You also don't have any methods written that handle the extraction of the data from the text fields into the dictionaries you're keeping. So, you need to wire a button to an IBAction method. This method will extract the text from the UITextField IBOutlets (self.stateField.text) and save to the NSMutableDictionary. Be sure your IBOutlets are connected properly to your view controller code and that the buttons are properly wired to your IBAction method. Those keywords should get you on the right track to solving the question.
Related: I see a lot of info about "websites" and "passwds" in your code. This leads me to think you've copy-pasted some other code and are trying to shoehorn your assignment into this code or else this used to be a different assignment and your instructor gave you a starting point that was poorly modified.
Bonus free editorial — It's not 100% clear if the instruction to keep two dictionaries with the same data in reverse order is inherent to the assignment, it's overkill IMHO since you have the allKeysForObject: method to get the key that corresponds to the object being searched. So, NSDictionary *capitalsDictionary = #{#"Texas": #"Austin"} can give you Austin for the key Texas with capitalsDictionary[#"Texas"] and can give you Texas for the value Austin with [capitalsDictionary allKeysForObject:#"Austin"][0] (The [0] is because allKeys… returns an array. If you check this before inserting a new capital, you can be sure that there will only be one. More than one would be a mistake unless somehow more than one state's capital city would be the same name, which is not the case in the USA, but might be elsewhere. By using allKeysForObject: you'd be able to handle this situation but with the multiple dictionaries, you wouldn't because a key can correspond to only one value. You could make that value an array, but it would still have the same problem as allKeys…

Dynamic button selection

Suppose I have two buttons that are ivar outlets. One is called "Blue" and the other "Red." Now, I have an NSString with the value of "Red." I want to set the button identified by the NSString to a selected state, without using if.
I do not want to do this:
NSString *button=#"Red";
if ([button isEqualtoString:#"Blue")
self.blue.selected=YES; //self.blue and self.red are UIButtons
else
self.red.selected=YES;
This is fine if you have two buttons, but I have quite a lot more than that, and it would be quite inelegant and cumbersome to do this for like 30 buttons.
I'd much rather find a way to directly link the name of a UIButton ivar to the value of an NSString.
Put the string lower case and
you can use KVC for that :
[self setValue:[NSNumber numberWithBool:YES]
forKeyPath:[NSString stringWithFormat:#"%#.selected", button]];
You can put the buttons in a dictionary :
[NSDictionaryName addObject: UIButtonName forKey: #"blue"];
or something like that :
[NSDictionaryName objectForKey:#"blue"].selected = YES;

NSTextField delegate notifications -- how to get text?

I've been trying to learn to use Xcode, but I'm getting confused with how to register that NSTextField has changed. Essentially, I have an NSTextField and a button. Clicking the button does stuff with the text in the field. However, I want to be able to get the text of the field without needing to use the text field "Action:send on end editing." That is, I want to be able to enter text and immediately press the button, without hitting enter or tabbing out of the text box. It seems like the way to do this would be by setting a delegate for my NSTextField that responds to
- (void)controlTextDidChange:(NSNotification *)aNotification
But I don't understand how to get the text that has been entered. I assume it has something to do with
[[aNotification userInfo] valueForKey:#"NSFieldEditor"];
but I really have no idea where to go from there.
You're on the right track! The object that you get out of the notification's user info dictionary is the Field Editor, which is simply an NSTextView that's handling the text input on the text field's behalf.
Once you have that object, all you have to do is ask it for its textStorage, which is an NSTextStorage* object holding the text. That object, in turn, has its string which is a plain old NSString holding just the characters.
NSTextView * fieldEditor = [[aNotification userInfo] objectForKey:#"NSFieldEditor"];
NSString * theString = [[fieldEditor textStorage] string];
*A subclass of NSAttributedString, which is an object holding a string and associated "attributes" like color, font, and underlining.
In your button action method, simply read the current string value in the text field:
- (IBAction)didClickTheButton:(id)sender {
NSString* theString = [myTextField stringValue];
// do something with theString
}
If you're only ever handling a single text field, this may be simpler:
- (void)controlTextDidChange:(NSNotification *)obj {
[self.inputField stringValue];
}
I'm totally ignoring all the complicated details of NSText and whatnot and just using the simplicity of the notification being sent and the simplicity of getting the string value from a text field.