How can I insert text into UITextField at the current cursor position? - objective-c

I'm trying to use the UITextField's "return" key to insert a custom character. Here's what my UITextFieldDelegate method looks like:
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
[textField insertText:#"¶"];
return NO;
}
Unfortunately, this only works some of the time:
"one two|" --> move cursor --> "one| two" --> return --> "one¶| two" (OK)
"onetwo|" --> return --> "onetwo¶|" (OK)
"onetwo|" --> move cursor --> "one|two" --> return --> "onetwo¶|" (FAIL)
In the last case I would have expected "one¶|two".
How do I ensure that the inserted text is always inserted at the cursor position?
Thanks.

The problem is when you tap the return key on the keyboard, the text field sets the selected range (the cursor position) to the end of its text before it sends you the textFieldShouldReturn: message.
You need to keep track of the cursor position so you can restore it to its prior position. Let's say you have a reference to the text field in a property:
#interface ViewController () <UITextFieldDelegate>
#property (strong, nonatomic) IBOutlet UITextField *textField;
#end
You'll need an instance variable to hold the prior selected text range (from before the return key was tapped):
#implementation ViewController {
UITextRange *priorSelectedTextRange_;
}
Then you can write a method that saves the selected text range to the instance variable:
- (void)saveTextFieldSelectedTextRange {
priorSelectedTextRange_ = self.textField.selectedTextRange;
}
and in textFieldShouldReturn:, before you insert the pilcrow, you can change the selected text range back to its prior value:
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
textField.selectedTextRange = priorSelectedTextRange_;
[textField insertText:#"¶"];
return NO;
}
But how can we make the system send the saveTextFieldSelectedTextRange message when we need it to?
The UITextFieldDelegate protocol doesn't have messages for changes to the selected range.
UITextField doesn't post any notifications for changes to the selected range.
The UITextInputDelegate protocol does have selectionWillChange: and selectionDidChange: messages, but the system sets the text field's inputDelegate to its own UIKeyboardImpl object when the text field begins editing, so we can't use the inputDelegate.
Key-value observing on the text field's selectedTextRange property isn't reliable. In my testing on the iOS 6.0 simulator, I don't get a KVO message when I move the cursor from the middle to the end of the text by tapping the text field.
The only way I can think of to reliably track changes to the text field's selected range is by adding an observer to the run loop. On every pass through the event loop, the observer runs before event processing, so it can grab the current selected range before it changes.
So we actually need another instance variable, to hold the reference to our run loop observer:
#implementation ViewController {
UITextRange *priorSelectedTextRange_;
CFRunLoopObserverRef runLoopObserver_;
}
We create the observer in viewDidLoad:
- (void)viewDidLoad {
[super viewDidLoad];
[self createRunLoopObserver];
}
and we destroy it in both viewDidUnload and in dealloc:
- (void)viewDidUnload {
[super viewDidUnload];
[self destroyRunLoopObserver];
}
- (void)dealloc {
[self destroyRunLoopObserver];
}
To create the observer, we need a plain old C function for it to call. Here's that function:
static void runLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
__unsafe_unretained ViewController *self = (__bridge ViewController *)info;
[self saveTextFieldSelectedTextRange];
}
Now we can actually create the observer and register it with the main run loop:
- (void)createRunLoopObserver {
runLoopObserver_ = CFRunLoopObserverCreate(NULL, kCFRunLoopAfterWaiting, YES, 0, &runLoopObserverCallback, &(CFRunLoopObserverContext){
.version = 0,
.info = (__bridge void *)self,
.retain = CFRetain,
.release = CFRelease,
.copyDescription = CFCopyDescription
});
CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver_, kCFRunLoopCommonModes);
}
and here's how we actually deregister the observer and destroy it:
- (void)destroyRunLoopObserver {
if (runLoopObserver_) {
CFRunLoopRemoveObserver(CFRunLoopGetMain(), runLoopObserver_, kCFRunLoopCommonModes);
CFRelease(runLoopObserver_);
runLoopObserver_ = NULL;
}
}
This approach works in my testing on the iOS 6.0 simulator.

What's happening here is that you're not keeping track of the insertion point, also known as the selection range.
And to do that, you need to get somewhat deeper into the guts of what UITextField can do.
Using UITextInput (accessible as a protocol that UITextField uses), you can fetch the "selectedTextRange" property which tells you where the caret (cursor, insertion point) is and that's where you should insert your special character. This should work if you set your object to be a delegate that conforms to the "UITextInput" protocol.

Related

Subclassing NSPopUpButton to add a bindable property

I'm trying to add a bindable property to a custom NSPopUpButton subclass.
I've created a "selectedKey" property, which is meant to store a NSString associated with selected menu item.
In control init, I set self as button target and an action for the button (valueChanged:), which in turn sets "selectedKey" in accordance with user selection:
#interface MyPopUpButton : NSPopUpButton {
NSMutableDictionary *_items;
NSString *_selectedKey;
}
#property(nonatomic, readwrite, copy) NSString* selectedKey;
- (void)addItemWithTitle:(NSString *)title andKey:(NSString *)key;
#end
#implementation MyPopUpButton
- (instancetype)initWithFrame:(NSRect)frameRect {
self = [super initWithFrame:frameRect];
if (self) {
_items = [NSMutableDictionary new];
[NSObject exposeBinding:#"selectedKey"];
[super setTarget:self];
[super setAction:#selector(valueChanged:)];
}
return self;
}
- (void)addItemWithTitle:(NSString *)title andKey:(NSString *)key {
[super addItemWithTitle:title];
[_items setValue:title forKey:key];
}
- (void)valueChanged:(id)sender {
for (NSString *aKey in [_items allKeys]) {
if ([[_items valueForKey:aKey] isEqualToString:[self titleOfSelectedItem]]) {
self.selectedKey = aKey;
}
}
}
- (void)setSelectedKey:(NSString *)selectedKey {
[self willChangeValueForKey:#"selectedKey"];
_selectedKey = selectedKey;
[self didChangeValueForKey:#"selectedKey"];
[self selectItemWithTitle:[_items valueForKey:selectedKey]];
}
#end
This seems to work as expected: "selectedKey" property is changed when user changes PopUpButton selection.
Unfortunately, trying to bind this property, doesn't work.
[selectButton bind:#"selectedKey" toObject:savingDictionary withKeyPath:key options:#{NSContinuouslyUpdatesValueBindingOption : #YES }]
When selection is changed bind object is not updated accordingly.
What am I doing wrong?
I've created a "selectedKey" property, which is meant to store an NSString associated with selected menu item.
Bindings is definitely the way to go here, but your use of bind:toObject:withKeyPath:options is incorrect.
The value that you pass to the first argument must be one of the predefined values made available by Apple for that particular control. For NSPopUpButton objects, the available values are documented in the NSPopUpButton Bindings Reference. When you look through this document you'll see that there is no selectedKey option. There is however a selectedValue which has the following description:
An NSString that specifies the title of the selected item in the NSPopUpButton.
Thus the correct way to set up the binding is as follows:
[self.btn bind:#"selectedValue"
toObject:self
withKeyPath:#"mySelectedString"
options:nil];
This is all you need to do: when the action selector is fired the property stored at the keyPath you passed in as the third argument will already have been updated. This means that you can (i) get rid of the setSelectedKey method entirely, (ii) remove exposeBinding line, and (iii) remove the code within valueChanged: - Cocoa has already done this bit.
The example below implements just two methods, but, if I've understood your intentions, they should be all you need:
- (void)awakeFromNib {
self.btn.target = self;
self.btn.action = #selector(popUpActivity:);
[self.btn bind:#"selectedValue"
toObject:self
withKeyPath:#"mySelectedString"
options:nil];
// I've added a couple of additional bindings here; they're
// not required, but I thought they'd be instructive.
[self.btn bind:#"content"
toObject:self
withKeyPath:#"myItems"
options:nil];
[self.btn bind:#"selectedIndex"
toObject:self
withKeyPath:#"mySelectedIndex"
options:nil];
// Now that you've set the bindings up, use them!
self.myItems = #[#"Snow", #"Falling", #"On", #"Cedars"];
self.mySelectedIndex = #3; // "Cedars" will be selected on startup
// no need to set value of mySelectedString, because it will be
// updated automatically by the selectedIndex binding.
NSLog("%#", self.mySelectedString) // -> "Cedars"
}
- (void)popUpActivity:(id)sender {
NSLog(#"value of <selectedIndex> -> %#", self.mySelectedIndex);
NSLog(#"value of <selectedString> -> %#", self.mySelectedString);
}
A final point worth making is that none of the above should be a part of an NSPopUpButton subclass. It looks like you can - and therefore should - do everything you need to do without a custom subclass of this control. In my demo-app the code above belongs to the ViewController class, you should try doing this also.

Use KVO for NSTextFields that are bound together

I'm having trouble getting KVO working with text fields that are bound together in a Cocoa app. I have gotten this to work when setting strings in NSTextFields with buttons but it is not working with bindings. As always, any help from Stack Overflow would be greatly appreciated.
Purpose of my code is to:
bind several text fields together
when a number is input in one field, have the other fields automatically update
observe the changes in the text fields
Here's my code for MainClass which is an NSObject subclass:
#import "MainClass.h"
#interface MainClass ()
#property (weak) IBOutlet NSTextField *fieldA;
#property (weak) IBOutlet NSTextField *fieldB;
#property (weak) IBOutlet NSTextField *fieldC;
#property double numA, numB, numC;
#end
#implementation MainClass
static int MainClassKVOContext = 0;
- (void)awakeFromNib {
[self.fieldA addObserver:self forKeyPath:#"numA" options:0 context:&MainClassKVOContext];
[self.fieldB addObserver:self forKeyPath:#"numB" options:0 context:&MainClassKVOContext];
[self.fieldC addObserver:self forKeyPath:#"numC" options:0 context:&MainClassKVOContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context != &MainClassKVOContext) {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
return;
}
if (object == self.fieldA) {
if ([keyPath isEqualToString:#"numA"]) {
NSLog(#"fieldA length = %ld", [_fieldA.stringValue length]);
}
}
if (object == self.fieldB) {
if ([keyPath isEqualToString:#"numB"]) {
NSLog(#"fieldB length = %ld", [_fieldB.stringValue length]);
}
}
if (object == self.fieldC) {
if ([keyPath isEqualToString:#"numC"]) {
NSLog(#"fieldC length = %ld", [_fieldC.stringValue length]);
}
}
}
+ (NSSet *)keyPathsForValuesAffectingNumB {
return [NSSet setWithObject:#"numA"];
}
+ (NSSet *)keyPathsForValuesAffectingNumC {
return [NSSet setWithObject:#"numA"];
}
- (void)setNumB:(double)theNumB {
[self setNumA:theNumB * 1000];
}
- (double)numB {
return [self numA] / 1000;
}
- (void)setNumC:(double)theNumC {
[self setNumA:theNumC * 1000000];
}
- (double)numC {
return [self numA] / 1000000;
}
- (void)setNilValueForKey:(NSString*)key {
if ([key isEqualToString:#"numA"]) return [self setNumA: 0];
if ([key isEqualToString:#"numB"]) return [self setNumB: 0];
if ([key isEqualToString:#"numC"]) return [self setNumC: 0];
[super setNilValueForKey:key];
}
#end
And here is the binding for one of the text fields:
Key-Value Observing on NSTextFields
In your -awakeFromNib method's implementation, you've written
[self.fieldA addObserver:self
forKeyPath:#"numA"
options:0
context:&MainClassKVOContext];
This doesn't do what you're hoping it will: self.fieldA is not key-value coding compliant for the key numA: if you try sending -valueForKey: or -setValue:forKey: with the key #"numA" to self.fieldA, you'll get the following exceptions:
[ valueForUndefinedKey:]: this class is not key value coding-compliant for the key numA.
and
[ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key numA.
As a result, the NSTextField instances are not key-value observing compliant for #"numA", either: the first requirement to be KVO-compliant for some key is to be KVC-compliant for that key.
It is, however, KVO-compliant for, among other things, stringValue. This allows you to do what I described earlier.
Note: None of this is altered by the way that you've set up bindings in Interface Builder. More on that later.
The Trouble With Key-Value Observing on NSTextField's stringValue
Observing an NSTextField's value for #"stringValue" works when -setStringValue: gets called on the NSTextField. This is a result of the internals of KVO.
A Brief Trip Into KVO Internals
When you begin observing an key-value observing an object for the first time, the object's class is changed--its isa pointer is changed. You can see this happening by overriding -addObserver:forKeyPath:options:context:
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context
{
NSLog(#"%p, %#", self->isa, NSStringFromClass(self->isa));
[super addObserver:observer
forKeyPath:keyPath
options:options
context:context];
NSLog(#"%p, %#", self->isa, NSStringFromClass(self->isa));
}
In general, the name of the class changes from Object to NSKVONotifying_Object.
If we had called -addObserver:forKeyPath:options:context: on an instance of Object with with the key path #"property"--a key for which instances of Object are KVC-compliant--when next we call -setProperty: on our instance of Object (in fact, now an instance of NSKVONotifying_Object), the following messages will be sent to the object
-willChangeValueForKey: passing #"property"
-setProperty: passing #"property"
-didChangeValueForKey: passing #"property"
Breaking within any of these methods reveal that they're called from the undocumented function _NSSetObjectValueAndNotify.
The relevance of all of this is that the method -observeValueForKeyPath:ofObject:change:context: is called on the observer that we added to our instance of Object for the key path #"property" from -didChangeValueForKey:. Here's the top of the stack trace:
-[Observer observeValueForKeyPath:ofObject:change:context:]
NSKeyValueNotifyObserver ()
NSKeyValueDidChange ()
-[NSObject(NSKeyValueObserverNotification) didChangeValueForKey:] ()
How does this relate to NSTextField and #"stringValue"?
In your previous setup, you were adding an observer to your text field on -awakeFromNib. This meant that your text field was already an instance of NSKVONotifying_NSTextField.
You would then press one or another button which in turn would call -setStringValue on your text field. You were able to observe this change because--as an instance of NSKVONotifying_NSTextField--your text field, upon receiving setStringValue:value actually received
willChangeValueForKey:#"stringValue"
setStringValue:value
didChangeValueForKey:#"stringValue"
As above, from within didChangeValueForKey:#"stringValue", all the objects which are observing the text field's value for #"stringValue" are notified that the value for this key has changed in their own implementations of -observeValueForKeyPath:ofObject:change:context:. In particular, this is true for the the object which you added as an observer for the text field in -awakeFromNib.
In summary, you were able to observe the change in the text field's value for #"stringValue" because you added yourself as an observer of the text field for that key and because -setStringValue was called on the text field.
So What's The Problem?
So far under the guise of discussing "The Trouble With Key-Value Observing on NSTextFields" we've only actually made sense of the opening sentence
Observing an NSTextField's value for #"stringValue" works when -setStringValue: gets called on the NSTextField.
And that sounds great! So what's the problem?
The problem is that -setStringValue: does not get called on the text field as the user is typing into it OR even after the user has ended editing (by tabbing out of the text field, for example). (Furthermore, -willChangeValueForKey: and -didChangeValueForKey: are not called manually. If they were, our KVO would work; but it doesn't.) This means that while our KVO on #"stringValue" works when -setStringValue: is called on the text field, it does NOT work when the user herself enters text.
TL;DR: KVO on the #"stringValue" of an NSTextField isn't good enough since it doesn't work for user input.
Binding An NSTextField's Value To A String
Let's try using bindings.
Initial Setup
Create an example project with a separate window controller (I've used the creative name WindowController) complete with XIB. (Here's the project I'm starting from on GitHub.) In WindowController.m added a property stringA in a class extension:
#interface WindowController ()
#property (nonatomic) NSString *stringA;
#end
In Interface Builder, create a text field and open the Bindings Inspector:
Under the "Value" header, expand the "Value" item:
The pop-up button next to the "Bind to" checkbox presently has "Shared User Defaults Controller" selected. We want to bind the text field's value to our WindowController instance., so select "File's Owner" instead. When this happens, the "Controller Key" field will be emptied and the "Model Key Path" field will be changed to "self".
We want to bind this text field's value to our WindowController instance's property stringA so change the "Model Key Path" to self.stringA:
At this point, we are done. (Progress so far on GitHub.) We have successfully bound the text field's value to our WindowController's property stringA.
Testing It Out
If we set stringA to some value in -init, that value will show up in the text field when the window loads:
- (id)init
{
self = [super initWithWindowNibName:#"WindowController"];
if (self) {
self.stringA = #"hello world";
}
return self;
}
And already, we have set up bindings in the other direction as well; upon ending editing in the text field, the our window controller's property stringA is set. We can check this by overriding it's setter:
- (void)setStringA:(NSString *)stringA
{
NSLog(#"%s: stringA: <<%#>> => <<%#>>", __PRETTY_FUNCTION__, _stringA, stringA);
_stringA = stringA;
}
Reply Hazy, Try Again
After typing some text into the text field and pressing tab, we'll see printed out
-[WindowController setStringA:]: stringA: <<(null)>> => <<some text>>
This looks great. Why haven't we been talking about this all along??? There's a bit of a hitch here: the pesky pressing tab thing. Binding a text field's value to a string does not set the string value until editing has ended in the text field.
A New Hope
However, there is still hope! The Cocoa Binding Documentation for NSTextField states that one binding option available for an NSTextField is NSContinuouslyUpdatesValueBindingOption. And lo and behold, there is a checkbox corresponding to this very option in the Bindings Inspector for NSTextField's value. Go ahead and check that box.
With this change in place, as we type things in, the update to the window controller's stringA property is continuously logged out:
-[WindowController setStringA:]: stringA: <<(null)>> => <<t>>
-[WindowController setStringA:]: stringA: <<t>> => <<th>>
-[WindowController setStringA:]: stringA: <<th>> => <<thi>>
-[WindowController setStringA:]: stringA: <<thi>> => <<thin>>
-[WindowController setStringA:]: stringA: <<thin>> => <<thing>>
-[WindowController setStringA:]: stringA: <<thing>> => <<things>>
-[WindowController setStringA:]: stringA: <<things>> => <<things >>
-[WindowController setStringA:]: stringA: <<things >> => <<things i>>
-[WindowController setStringA:]: stringA: <<things i>> => <<things in>>
Finally, we're continuously updating the window controller's string from the text field. The rest is easy. As a quick proof of concept, add a couple more text fields to the window, bind them to stringA and set them to update continuously. You at this point have three synchronized NSTextFields! Here's the project with three synchronized text fields.
The Rest of the Way
You're wanting to setup three textfields that display numbers that have some relationship to each other. Since we're dealing with numbers now, we'll remove the property stringA from WindowController and replace it with numberA, numberB and numberC:
#interface WindowController ()
#property (nonatomic) NSNumber *numberA;
#property (nonatomic) NSNumber *numberB;
#property (nonatomic) NSNumber *numberC;
#end
Next we'll bind the first text field to numberA on File's Owner, the second to numberB, and so on. Finally we just need to add a property which is the quantity which is being represented in these different ways. Let's call that value quantity.
#interface WindowController ()
#property (nonatomic) NSNumber *quantity;
#property (nonatomic) NSNumber *numberA;
#property (nonatomic) NSNumber *numberB;
#property (nonatomic) NSNumber *numberC;
#end
We'll need the constant conversion factors to transform from the units of quantity to the units of numberA and so forth, so add
static float convertToA = 1000.0f;
static float convertToB = 573.0f;
static float convertToC = 720.0f;
(Of course, use the numbers that are relevant to your situation.) With this much, we can implement the accessors for each of the numbers:
- (NSNumber *)numberA
{
return [NSNumber numberWithFloat:self.quantity.floatValue * convertToA];
}
- (void)setNumberA:(NSNumber *)numberA
{
self.quantity = [NSNumber numberWithFloat:numberA.floatValue * 1.0f/convertToA];
}
- (NSNumber *)numberB
{
return [NSNumber numberWithFloat:self.quantity.floatValue * convertToB];
}
- (void)setNumberB:(NSNumber *)numberB
{
self.quantity = [NSNumber numberWithFloat:numberB.floatValue * 1.0f/convertToB];
}
- (NSNumber *)numberC
{
return [NSNumber numberWithFloat:self.quantity.floatValue * convertToC];
}
- (void)setNumberC:(NSNumber *)numberC
{
self.quantity = [NSNumber numberWithFloat:numberC.floatValue * 1.0f/convertToC];
}
All of the different number accessors are now just indirect mechanisms for accessing quantity, and are perfect for bindings. There is only one additional thing that remains to be done: we need to make sure that observers repoll all of the numbers whenever quantity is changed:
+ (NSSet *)keyPathsForValuesAffectingNumberA
{
return [NSSet setWithObject:#"quantity"];
}
+ (NSSet *)keyPathsForValuesAffectingNumberB
{
return [NSSet setWithObject:#"quantity"];
}
+ (NSSet *)keyPathsForValuesAffectingNumberC
{
return [NSSet setWithObject:#"quantity"];
}
Now, whenever you type into one of the textfields, the others are updated accordingly. Here's the final version of the project on GitHub.

Setting/getting global variables in objective-C

I am writing an app which is a sort of dictionary - it presents the user with a list of terms, and when clicked on, pops up a dialog box containing the definition. The definition itself may also contain terms, which in turn the user can click on to launch another definition popup.
My main app is stored in 'myViewController.m'. It calls a custom UIView class, 'CustomUIView.m' to display the definition (this is the dialog box that pops up). This all works fine.
The text links from the CustomUIView then should be able to launch more definitions. When text is tapped in my CustomUIView, it launches another CustomUIView. The problem is, that this new CustomUIView doesn't have access to the hash map which contains all my dictionary's terms and definitions; this is only available to my main app, 'myViewController.m'.
Somehow, I need to make my hash map, dictionaryHashMap, visible to every instance of the CustomUIView class. dictionaryHashMap is created in myViewController.m when the app opens and doesn't change thereafter.
I don't wish to limit the number of CustomUIViews that can be opened at the same time (I have my reasons for doing this!), so it would be a little resource intensive to send a copy of the dictionaryHashMap to every instance of the CustomUIView. Presumably, the solution is to make dictionaryHashMap a global variable.
Some of my code:
From myViewController.m:
- (void)viewDidLoad
{
self.dictionaryHashMap = [[NSMutableDictionary alloc] init]; // initialise the dictionary hash map
//... {Code to populate dictionaryHashMap}
}
// Method to pop up a definition dialog
- (void)displayDefinition:(NSString *) term
{
NSArray* definition = [self.dictionaryHashMap objectForKey:term]; // get the definition that corresponds to the term
CustomUIView* definitionPopup = [[[CustomUIView alloc] init] autorelease]; // initialise a custom popup
[definitionPopup setTitle: term];
[definitionPopup setMessage: definition];
[definitionPopup show];
}
// Delegation for sending URL presses in CustomUIView to popupDefinition
#pragma mark - CustomUIViewDelegate
+ (void)termTextClickedOn:(CustomUIView *)customView didSelectTerm:(NSString *)term
{
myViewController *t = [[myViewController alloc] init]; // TODO: This instance has no idea what the NSDictionary is
[t displayDefinition:term];
}
From CustomUIView.m:
// Intercept clicks on links in UIWebView object
- (BOOL)webView: (UIWebView*)webView shouldStartLoadWithRequest: (NSURLRequest*)request navigationType: (UIWebViewNavigationType)navigationType {
if ( navigationType == UIWebViewNavigationTypeLinkClicked ) {
[myViewController termTextClickedOn:self didSelectTerm:request];
return NO;
}
return YES;
}
Any tips on how to make the dictionaryHashMap visible to CustomUIView would be much appreciated.
I have tried making the dictionaryHashMap global by doing the following:
Changing all instances of 'self.dictionaryHashMap' to 'dictionaryHashMap'
Adding the line 'extern NSMutableDictionary *dictionaryHashMap;' to CustomUIView.h
Adding the following outside of my implementation in myViewController.m: 'NSMutableDictionary *dictionaryHashMap = nil;'
However, the dictionaryHashMap remains invisible to CustomUIView. As far as I can tell, it actually remains a variable which is local to myViewController...
It's not resource-intensive to pass around the reference (pointer) to dictionaryHashMap. A pointer to an object is only 4 bytes. You could just pass it from your view controller to your view.
But I don't know why you even need to do that. Your view is sending a message (termTextClickedOn:didSelectTerm:) to the view controller when a term is clicked. And the view controller already has a reference to the dictionary, so it can handle the lookup. Why does the view also need a reference to the dictionary?
Anyway, if you want to make the dictionary a global, it would be more appropriate to initialize it in your app delegate, in application:didFinishLaunchingWithOptions:. You could even make the dictionary be a property of your app delegate and initialize it lazily.
UPDATE
I didn't notice until your comment that termTextClickedOn:didSelectTerm: is a class method. I assumed it was an instance method because myViewController starts with a lower-case letter, and the convention in iOS programming is that classes start with capital letters. (You make it easier to get good help when you follow the conventions!)
Here's what I'd recommend. First, rename myViewController to MyViewController (or better, DefinitionViewController).
Give it a property that references the dictionary. Whatever code creates a new instance of MyViewController is responsible for setting this property.
Give CustomUIView properties for a target and an action:
#property (nonatomic, weak) id target;
#property (nonatomic) SEL action;
Set those properties when you create the view:
- (void)displayDefinition:(NSString *)term {
NSArray* definition = [self.dictionaryHashMap objectForKey:term];
CustomUIView* definitionPopup = [[[CustomUIView alloc] init] autorelease]; // initialise a custom popup
definitionPopup.target = self;
definitionPopup.action = #selector(termWasClicked:);
...
In the view's webView:shouldStartLoadWithRequest: method, extract the term from the URL request and send it to the target/action:
- (BOOL)webView: (UIWebView*)webView shouldStartLoadWithRequest: (NSURLRequest*)request navigationType: (UIWebViewNavigationType)navigationType {
if ( navigationType == UIWebViewNavigationTypeLinkClicked ) {
NSString *term = termForURLRequest(request);
[self.target performSelector:self.action withObject:term];
return NO;
}
return YES;
}
In the view controller's termWasClicked: method, create the new view controller and set its dictionary property:
- (void)termWasClicked:(NSString *)term {
MyViewController *t = [[MyViewController alloc] init];
t.dictionary = self.dictionary;
[t displayDefinition:term];
}
Create a class that will be used as singleton. Example.
You Should always keep your data in separate class as the mvc pattern suggest and that could be achieved by using a singleton class for all your dictionary terms and accesing them from every custom view when needed.

Want to make UITextView react to return key

I'm trying to implement a program in Xcode that's somewhat like a command line. I basically have a UITextView that can take multiple lines of text. Right now, I have a button that will make the necessary changes after the user is done entering commands, but I want to be able to have a method that gets called after the user hits the return key in the UITextView, so basically it makes changes after each command. Is it possible to do this?
The BOOL method mentioned above is a wrong answer... for one the person is checking the text from the TextView the moment before it is updated so they are viewing the old text... Also the methods are out of date.
This usage will work immediately once the return key is pressed (The current "answer" will not work until after the return key has been pressed and then ANOTHER key is pressed):
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
if ([text isEqualToString:#"\n"]) {
NSLog(#"Return pressed");
} else {
NSLog(#"Other pressed");
}
return YES;
}
Don't forget to add the UITextViewDelegate to your .h file's protocols.
#interface ViewController : UIViewController <UITextViewDelegate> {
and set yourTextView.delegate = self; in .m file!
/*
note: This will also get called if a user copy-pastes just a line-break...
unlikely but possible. If you need to ignore pasted line-breaks for some
reason see here: http://stackoverflow.com/a/15933860/2057171 ...
Now for an unrelated-tip: If you want to accept pasted line breaks however
I suggest you add an "or" to the conditional statement and make it also
check if "text" isEqualToString #"\r" which is another form of line-break
not found on the iOS keyboard but it can, however, be found on a website
and copy-pasted into your textView. If you want to accept pasted text
with a line-break at the end you will need to change the
"isEqualToString" code above to say "hasSuffix", this will check for
any string %# with a "\n" at the end. (and again for "\r") but make
sure you don't call your "next" method until after `return YES;`
has been called and the text view has been updated, otherwise
you will get only the text that was there before the copy paste
since this is "shouldChangeTextInRange" method, not
"didChangeTextInRange", if you do this I suggest stripping the
"\n" or "\r" from the end of your final string after the copy-paste
was made and applied and the text was updated.
*/
If you set a delegate to the UITextView which implements
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
Then you can check if the last character is "\n", and get the text entered since the last command by doing
NSArray* components = [textView.text componentsSeparatedByString:#"\n"];
if ([components count] > 0) {
NSString* commandText = [components lastObject];
// and optionally clear the text view and hide the keyboard...
textView.text = #"";
[textView resignFirstResponder];
}
Note, I haven't tested this, just an idea:
- (void)textViewDidChange:(UITextView *)textView
{
if ([[textView.text substringWithRange:NSMakeRange(textView.text.length - 1, 1)] isEqualToString:#"\n"])
{
[textView resignFirstResponder];
[self methodYouWantToCall];
}
}
You can do this by setting up a delegate for the UITextView. See UITextViewDelegate Protocol Reference for details on what can be done.

Arrow keys with NSTableView

Is it possible to navigate an NSTableView's editable cell around the NSTableView using arrow keys and enter/tab? For example, I want to make it feel more like a spreadsheet.
The users of this application are expected to edit quite a lot of cells (but not all of them), and I think it would be easier to do so if they didn't have to double-click on each cell.
In Sequel Pro we used a different (and in my eyes simpler) method: We implemented control:textView:doCommandBySelector: in the delegate of the TableView. This method is hard to find -- it can be found in the NSControlTextEditingDelegate Protocol Reference. (Remember that NSTableView is a subclass of NSControl)
Long story short, here's what we came up with (we didn't override left/right arrow keys, as those are used to navigate within the cell. We use Tab to go left/right)
Please note that this is just a snippet from the Sequel Pro source code, and does not work as is
- (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)command
{
NSUInteger row, column;
row = [tableView editedRow];
column = [tableView editedColumn];
// Trap down arrow key
if ( [textView methodForSelector:command] == [textView methodForSelector:#selector(moveDown:)] )
{
NSUInteger newRow = row+1;
if (newRow>=numRows) return TRUE; //check if we're already at the end of the list
if (column>= numColumns) return TRUE; //the column count could change
[tableContentView selectRowIndexes:[NSIndexSet indexSetWithIndex:newRow] byExtendingSelection:NO];
[tableContentView editColumn:column row:newRow withEvent:nil select:YES];
return TRUE;
}
// Trap up arrow key
else if ( [textView methodForSelector:command] == [textView methodForSelector:#selector(moveUp:)] )
{
if (row==0) return TRUE; //already at the beginning of the list
NSUInteger newRow = row-1;
if (newRow>=numRows) return TRUE;
if (column>= numColumns) return TRUE;
[tableContentView selectRowIndexes:[NSIndexSet indexSetWithIndex:newRow] byExtendingSelection:NO];
[tableContentView editColumn:column row:newRow withEvent:nil select:YES];
return TRUE;
}
Well it isn't easy but I managed to do it without having to use RRSpreadSheet or even another control. Here's what you have to do:
Create a subclass of NSTextView, this will be the field editor. For this example the name MyFieldEditorClass will be used and myFieldEditor will refer to an instance of this class.
Add a method to MyFieldEditorClass called "- (void) setLastKnownColumn:(unsigned)aCol andRow:(unsigned) aRow" or something similar, and have it save both the input parameter values somewhere.
Add another method called "setTableView:" and have it save the NSTableView object somewhere, or unless there is another way to get the NSTableView object from the field editor, use that.
Add another method called - (void) keyDown:(NSEvent *) event. This is actually overriding the NSResponder's keyDown:. The source code should be (be aware that StackOverflow's MarkDown is changing < and > to < and >):
- (void) keyDown:(NSEvent *) event
{
unsigned newRow = row, newCol = column;
switch ([event keyCode])
{
case 126: // Up
if (row)
newRow = row - 1;
break;
case 125: // Down
if (row < [theTable numberOfRows] - 1)
newRow = row + 1;
break;
case 123: // Left
if (column > 1)
newCol = column - 1;
break;
case 124: // Right
if (column < [theTable numberOfColumns] - 1)
newCol = column + 1;
break;
default:
[super keyDown:event];
return;
}
[theTable selectRow:newRow byExtendingSelection:NO];
[theTable editColumn:newCol row:newRow withEvent:nil select:YES];
row = newRow;
column = newCol;
}
Give the NSTableView in your nib a delegate, and in the delegate add the method:
- (BOOL) tableView:(NSTableView *)aTableView shouldEditColumn:(NSTableColumn *) aCol row:aRow
{
if ([aTableView isEqual:TheTableViewYouWantToChangeBehaviour])
[myFieldEditor setLastKnownColumn:[[aTableView tableColumns] indexOfObject:aCol] andRow:aRow];
return YES;
}
Finally, give the Table View's main window a delegate and add the method:
- (id) windowWillReturnFieldEditor:(NSWindow *) aWindow toObject:(id) anObject
{
if ([anObject isEqual:TheTableViewYouWantToChangeBehaviour])
{
if (!myFieldEditor)
{
myFieldEditor = [[MyFieldEditorClass alloc] init];
[myFieldEditor setTableView:anObject];
}
return myFieldEditor;
}
else
{
return nil;
}
}
Run the program and give it a go!
Rather than forcing NSTableView to do something it wasn't designed for, you may want to look at using something designed for this purpose. I've got an open source spreadsheet control which may do what you need, or you may at least be able to extend it to do what you need: MBTableGrid
I wanted to reply to the answers here but the reply button seems to be missing so I'm forced to proved an answer when I really just want to ask a question about the replies.
Anyway, I've seen a few answers for overriding the -keyDown event of the table view that say to subclass the TableView but according to every Objective-C book I've read so far, and several Apple training videos, you should very rarely if ever subclass one of the core classes. In fact every single one of them makes the point that C programmers have a fascination with subclassing and that's not how Objective-C works; that Objective-C is all about helpers and delegates not subclassing.
So, should I just ignore any of the responses that say to subclass as this seems to be in direct contradiction to the precepts of Objective-C?
--- Edit ---
I found something that worked without subclassing the NSTableView. While I do move the inheritance up one notch on the chain from NSObject to NSResponder I'm not totally subclassing the NSTableView. I'm just adding the ability to override the keyDown event.
I made the class I was using as a delegate inherit from NSResponder instead of NSObject and set the nextResponder to that class in awakeFromNib. I was then able to trap key presses using the keydown event. I of course connected the IBOutlet and set the delegate in Interface Builder.
Here's my code with the minimum needed to show the trapping of the key:
Header file
// AppController.h
#import <Cocoa/Cocoa.h>
#interface AppController : NSResponder {
IBOutlet NSTableView *toDoListView;
NSMutableArray *toDoArray;
}
-(int)numberOfRowsInTableView:(NSTableView *)aTableView;
-(id)tableView:(NSTableView *)tableView
objectValueForTableColumn:(NSTableColumn *)aTableColumn
row:(int)rowIndex;
#end
Here's the m file.
// AppController.m
#import "AppController.h"
#implementation AppController
-(id)init
{
[super init];
toDoArray = [[NSMutableArray alloc] init];
return self;
}
-(void)dealloc
{
[toDoArray release];
toDoArray = nil;
[super dealloc];
}
-(void)awakeFromNib
{
[toDoListView setNextResponder:self];
}
-(int)numberOfRowsInTableView:(NSTableView *)aTableView
{
return [toDoArray count];
}
-(id)tableView:(NSTableView *)tableView
objectValueForTableColumn:(NSTableColumn *)aTableColumn
row:(int)rowIndex
{
NSString *value = [toDoArray objectAtIndex:rowIndex];
return value;
}
- (void)keyDown:(NSEvent *)theEvent
{
//NSLog(#"key pressed: %#", theEvent);
if (theEvent.keyCode == 51 || theEvent.keyCode == 117)
{
[toDoArray removeObjectAtIndex:[toDoListView selectedRow]];
[toDoListView reloadData];
}
}
#end