Parse and create an NSAttributedString when an UITextView's text changes - objective-c

I'm trying to parse certain parts of the string when a user types into an UITextView or the setText: method is called, and then setting an NSAttributedString back into the text view. However in my current implementation this causes an infinite recursive loop. Since setting the new attributed text causes the text to change (and the notification to fire) whereby I then re-parse the text.
Somebody suggested I use some kind of flag, so while i'm parsing and setting the text, I don't keep doing it. Though this doesn't seem like the optimal solution. Here's a snippet of my code...
CustomTextView.h (UITextView subclass)
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(textViewDidChange:) name:NSTextViewTextDidChangeNotification object:self];
CustomTextView textViewDidChange:
- (void)textViewDidChange:(NSNotification *)notification;
{
__block NSString *string = self.text;
dispatch_async(parserQueue, ^{
NSAttributedString *parsedString = [self parseAttributesForString:string];
dispatch_async(dispatch_get_main_queue(), ^{
[self setAttributedText:parsedString];
});
});
}
CustomTextView setText:
- (void)setText:(NSString *)text
{
[super setText:text];
[self textViewDidChange:nil];
}
Thanks!

Okay so first I'd add an observer to the property text instead of subclassing the class and post a notification. Next, I'd just check what class the text object is. I would this by calling [text isKindOfClass:[NSString class]]. By calling this you know whether the object needs to get parsed again.

Related

Force NSTextField to send textDidEndEditing

I have a subclass of NSTextField that I made so that when a user is done editing the field, the text field will lose focus. I also have it set up so whenever the user clicks on the main view, this will act as losing focus on the textfield. And this all works great. Now I want to add some additional capabilities to the subclass.
I want the textfield to send a textDidEndEditing every time a user clicks anywhere outside of the box. This includes when a user clicks on another UI component. The behavior I'm seeing right now is that when a user clicks on another UI component (let's say a combo box) the action does not trigger. Is there a way to force this? Besides manually adding it as a part of the other components actions?
Any help would be appreciated!
Here's the code for my textDidEndEditing function
- (void)textDidEndEditing:(NSNotification *)notification
{
NSString *file = nil;
char c = ' ';
int index = 0;
[super textDidEndEditing:notification];
if ([self isEditable])
{
// is there a valid string to display?
file = [self stringValue];
if ([file length] > 0)
{
c = [file characterAtIndex:([file length] - 1)];
if (c == '\n') // check for white space at the end
{
// whitespace at the end... remove
NSMutableString *newfile = [[NSMutableString alloc] init];
c = [file characterAtIndex:index++];
do
{
[newfile appendFormat:#"%c", c];
c = [file characterAtIndex:index++];
}
while ((c != '\n') && (index < [file length]));
[self setStringValue:newfile];
file = newfile;
}
[[NSNotificationCenter defaultCenter]
postNotificationName:#"inputFileEntered" object:self];
}
}
// since we're leaving this box, show no text in this box as selected.
// and deselect this box as the first responder
[self setSelectedText:0];
[[NSNotificationCenter defaultCenter]
postNotificationName:#"setResponderToNil" object:self];
}
Where "setSelectedText" is a public function in the text field subclass:
- (void)setSelectedText:(int) length
{
int start = 0;
NSText *editor = [self.window fieldEditor:YES forObject:self];
NSRange range = {start, length};
[editor setSelectedRange:range];
}
And the "setResponderToNil" notification is a part of my NSView subclass:
- (void)setResponderToNil
{
AppDelegate *delegate = (AppDelegate *)[NSApp delegate];
[delegate.window makeFirstResponder:nil];
}
I think I found a way to do this. It may not be the most eloquent, but it seems to work with the type of behavior I want.
I added an mouse event listener to the app's main controller:
event_monitor_mousedown_ = [NSEvent addLocalMonitorForEventsMatchingMask:NSRightMouseDown
handler:^NSEvent *(NSEvent * event)
{
NSResponder *resp = [[[NSApplication sharedApplication] keyWindow] firstResponder];
if ([resp isKindOfClass:[NSTextView class]])
{
// set UI in proper state - remove focus from text field
// even when touching a new window for the first time
[[NSNotificationCenter defaultCenter]
postNotificationName:#"setResponderToNil" object:self];
[self setStopState];
}
return event;
}];
This event checks the current responder in the application on any mouseDown action. If it's a textView object (which is type of the object that would be the first responder when editing an NSTextField) it will send the notification to set the firstResponder to nil. This forces the textDidEndEditing notification. I want to play around with it some more to see if I'm getting the right expected behavior. I hope this helps someone out there!

Can't set text in NSTextView from .xib

Below is a simplistic Obj-C / Cocoa "hello world" window, which is initialized from within a Carbon app. The .xib contains a NSWindow, which has an NSView containing the NSButton/NSButtonCell and a NSScrollView/NSTextView/NSScroller(s).
The code compiles and links with no warnings. The window is displayed properly, with both objects (button and text field). Pressing the button does indeed go to buttonWasPressed, and I receive no errors regarding bad selectors in Xcode's debugger.
But the text in the NSTextView is unchanged.
I THINK I have the proper outlet for myTextView connected. Perhaps using replaceTextContainer is not a proper way to connect myTextView to textContainer?
SAD NOTE: my 30 years of C++ programming does not a smooth transition to Obj-C/Cocoa make...
#implementation DictionaryWindowController
- (id)init {
self = [super init];
// This is actually a separate Cocoa window in a Carbon app -- I load it from the NIB upon command from a Carbon menu event...
NSApplicationLoad();
if (![NSBundle loadNibNamed:#"Cocoa Test Window.nib" owner:self]) {
NSLog(#"failed to load nib");
}
if (self) {
// textStorage is a NSTextStorage* in DictionaryWindowController (NSObject)
textStorage = [[NSTextStorage alloc] initWithString:#""];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[givenStorage addLayoutManager:layoutManager];
[layoutManager autorelease];
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithContainerSize:NSMakeSize(kLargeWidthForTextContainer, LargeNumberForText)];
[layoutManager addTextContainer:textContainer];
[textContainer autorelease];
// Is this how to "connect" the myTextView (NSTextView) from the .nib to the textStorage/layoutManager/textContainer?
[myTextView replaceTextContainer:textContainer];
[myTextView setMaxSize:NSMakeSize(LargeNumberForText, LargeNumberForText)];
[myTextView setSelectable:YES];
[myTextView setEditable:YES];
[myTextView setRichText:YES];
[myTextView setImportsGraphics:YES];
[myTextView setUsesFontPanel:YES];
[myTextView setUsesRuler:YES];
[myTextView setAllowsUndo:YES];
// shouldn't I be able to set the string in the NSTextStorage instance and cause the NSTextView to change its text and redraw?
[[textStorage mutableString] setString:#"Default text from initialization..."];
}
return self;
}
- (IBAction)buttonWasPressed:(id)sender {
// Pressing the button DOES get to this point, but the NSTextView didn't change...
[[textStorage mutableString] setString:#"After button press, this text should be the content of the NSTextView."];
}
#end
I believe you're "editing the text storage behind the text view's back". See NSTextStorage's -edited:range:changeInLength: documentation. The docs are a bit vague here but I believe -setString: on the -mutableString you're requesting does not notify the text storage itself of a change (it only updates attribute runs, etc. when modified).
Call -edited:range:changeInLength: or use NSTextView's -setString: method as Mike C. recommended.

Detecting a change to text in UITextfield

I would like to be able to detect if some text is changed in a UITextField so that I can then enable a UIButton to save the changes.
Instead of observing notifications or implementing textField:shouldChangeCharacterInRange:replacementString:, it's easier to just add an event target:
[textField addTarget:self
action:#selector(myTextFieldDidChange:)
forControlEvents:UIControlEventEditingChanged];
- (void)myTextFieldDidChange:(id)sender {
// Handle change.
}
Note that the event is UIControlEventEditingChanged and not UIControlEventValueChanged!
The advantages over the other two suggested solutions are:
You don't need to remember to unregister your controller with the NSNotificationCenter.
The event handler is called after the change has been made which means textField.text contains the text the user actually entered. The textField:shouldChangeCharacterInRange:replacementString: delegate method is called before the changes have been applied, so textField.text does not yet give you the text the user just entered – you'd have to apply the change yourself first.
Take advantage of the UITextFieldTextDidChange notification or set a delegate on the text field and watch for textField:shouldChangeCharactersInRange:replacementString.
If you want to watch for changes with a notification, you'll need something like this in your code to register for the notification:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(textFieldDidChange:) name:UITextFieldTextDidChangeNotification object:theTextField];
Here theTextField is the instance of UITextField that you want to watch. The class of which self is an instance in the code above must then implement textFieldDidChange, like so:
- (void)textFieldDidChange:(NSNotification *)notification {
// Do whatever you like to respond to text changes here.
}
If the text field is going to outlive the observer, then you must deregister for notifications in the observer's dealloc method. Actually it's a good idea to do this even if the text field does not outlive the observer.
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
// Other dealloc work
}
For that, first you need to have your textfield have it delegate reference assigned. And the delgate, should preferably be, the vew controller which is the files owner of the view.
Which goes like
myTextField.delegate = myViewControllerReferenceVariable
And in your viewController interface, tell you will be implementing UITextFieldDelegate by
#interface MyViewController:UIViewController<UITextFieldDelegate>
And in your view controller implementation override
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
So the code will look like
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
text = [textfield.text stringByReplacingCharactersInRange:range withString:string];
if (textfield == refToTextFieldYouWantToCheck) {
if ( ! [textToCheck isEqualToString:text] ) {
[theButtonRef setEnabled:YES];
}
}
return YES; //If you don't your textfield won't get any text in it
}
You can also subscribe to notification which is sort of messy IMHO
You can find how to do it here.
Swift 3.0
Process 1
Create IBOutlet of UITextfiled and Add Target to text field.
m_lblTxt.addTarget(self, action: #selector(self.textFieldDidChange), for: UIControlEvents.editingChanged)
func textFieldDidChange(textField:UITextField)
{
NSLog(textField.text!)
}
Process 2
m_lblTxt.delegate = self
//MARK: - TextField Delegates
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool
{
print(textField.text!)
return true
}
This can be accomplished in Interface Builder on the Editing Changed event of UITextField. Drag from it to your code and create an IBAction.
For example:
#IBAction func textFieldChanged(_ sender: UITextField) {
print(sender.text)
}
This event is the same as described in other answers here in that the .text property contains the updated text input when it gets triggered. This can help clean up code clutter by not having to programmatically add the event to every UITextField in the view.
You could create a variable to store the original string, then register with the notification center to receive UITextFieldTextDidChangeNotification event:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(updateButton:) name:UITextFieldTextDidChangeNotification object:nil];
Then, create a method to receive the notification, and compare the current value of the text field with the original value
-(void) updateButton:(NSNotification *)notification {
self.myButton.enabled = ![self.myTextField.text isEqualToString:originalString];
}
Don't forget to de-register the notification when the view controller is deallocated.
[[NSNotificationCenter defaultCenter] removeObserver:self name:UITextFieldTextDidChangeNotification object:nil];
You can add a class member like this NSString *changeTemp,then
changetemp = textfield;
if( change temp != textfild ){
changetemp=textfild;
NSLog(#" text is changed"
} else {
NSLog(#" text isn't changed"):
}

Working with a button in a custom tableview Cell

I have a facebook like button in a custom tableview cell. In the class of my tableview I have the following function.
- (IBAction)sendLike:(id)sender
WithString: (NSString *)shareString
andUrl:(NSURL *)shareUrl{
//Do all kinds of things
[fbController setInitialText:shareString];
[fbController addURL:shareUrl];
[fbController setCompletionHandler:completionHandler];
[self presentViewController:fbController animated:YES completion:nil];
}
}
In my cellForRowAtIndexpath I am trying to call this method in the following way.
NSString *shareString = video.name;
NSURL *shareUrl = [NSURL URLWithString:video.url];
[cell.facebook addTarget:self action:#selector(sendLike: WithString:shareString andUrl:shareUrl)
forControlEvents:UIControlEventTouchUpInside];
But it is complaining that I should put ':' in my #selector. Can anybody help ?
Thanks in advance.
You can try like this
cell.facebook.tag = indexPath.row.
[cell.facebook addTarget:self action:#selector(sendLike:)
forControlEvents:UIControlEventTouchUpInside];
in sendLike event
- (IBAction)sendLike:(id)sender
{
//if you want to fetch data from any list then try
//UIButton *selectedButton = (UIButton*)sender;
//data = [dataList objectAtIndex:selectedButton.tag];
NSString *shareString = video.name;
NSURL *shareUrl = [NSURL URLWithString:video.url];
[fbController setInitialText:shareString];
[fbController addURL:shareUrl];
[fbController setCompletionHandler:completionHandler];
[self presentViewController:fbController animated:YES completion:nil];
}
In your cellForRow:
//wiring a custom string to a button
objc_setAssociatedObject(yourButton, "theKey", strText, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
In your button handler:
NSString *str = objc_getAssociatedObject(sender, "theKey");
p.s don't forget to import runtime library:
#import <objc/runtime.h>
I think you invoked the selector in wrong way. You actually should write,
[cell.facebook addTarget:self action:#selector(sendLike: WithString: andUrl:)
forControlEvents:UIControlEventTouchUpInside];
But this doesn't pass arguments for you. If you want to pass arguments, you should call performSelector like
[self performSelector:#selector(sendLike: WithString: andUrl:)
withObject: cell.facebook
withObject: shareString
withObject: shareUrl];
Hope it will help solving your problem.
As its in the apple documentation, the only available specifications for an #selector in an action are the following methods:
- (void)action;
- (void)action:(id)sender;
- (void)action:(id)sender forEvent:(UIEvent *)event;
As you can see, your selector doesn't match this, because you have multiple arguments, but only the sender id is allowed.
As a solution, you can probably set a tag for your button and then handle it in your selector.
Your problem is that the number and kind of arguments are incorrect in your selector. You can only use targets with the following scheme:
- (void)action;
- (void)action:(id)sender;
- (void)action:(id)sender forEvent:(UIEvent *)event;
You will have to include your information in the sender (the UITableViewCell class instance) object. You can do that as described in the other answer by Stas, but I would recommend you to create a custom table view cell subclass which adds a property for shareString and shareURL. This approach will require more code, but make it much easier to read, maintain and prevents this ugly Obj-C and C mix up.

How do i correct this warning?

I was searching for a Done button for the Number Pad,then i saw this question:
How to show "Done" button on iPhone number pad
I copied Archie's answer code into mine,and i get 2 warnings in this area:
- (void)textFieldDidBeginEditing:(NSNotification *)note {
[self updateKeyboardButtonFor:[note object]];
}
- (void)keyboardWillShow:(NSNotification *)note {
[self updateKeyboardButtonFor:[self findFirstResponderTextField]];
}
- (void)keyboardDidShow:(NSNotification *)note {
[self updateKeyboardButtonFor:[self findFirstResponderTextField]];
}
The warnings are:
Incompatible Objective-C types initializing 'struct NSNotification *', expected 'struct UITextField *'
How can i correct that? I tried to switch with a UITextField but it all messed up
As BoltClock suggested, it does seem a bit strange that Archie use a delegate method's name as a notification handler. The problem might be stemming from the fact that you must be adopting the UITextFieldDelegate protocol. If you've done so, remove the line observing the notification,
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(textFieldDidBeginEditing:)
name:UITextFieldTextDidBeginEditingNotification
object:nil];
and then edit make the textFieldDidBeginEditing: method while becoming the delegate of the text fields,
- (void)textFieldDidBeginEditing:(UITextField *)textField {
[self updateKeyboardButtonFor:textField];
}
Or alternatively, rename the occurrences of textFieldDidBeginEditing: with some other suitable method name
textFieldDidBeginEditing is not a notification, it is a delegate method. The expected signature is - (void)textFieldDidBeginEditing:(UITextField *)aTextField