I am getting a strange behavior setting the selectedRange property for a textView within the textViewDidChangeSelection delegate.
My code in viewDidLoad is:
hiddenTextView = [[UITextView alloc] initWithFrame:CGRectMake(0, 0, 500, 500)];
//[hiddenTextView setHidden:YES];
_hiddenTextViewText=#"ulrd";
hiddenTextView.text = _hiddenTextViewText;
hiddenTextView.delegate = self;
_hiddenTextViewDefaultRange = NSMakeRange(2,0);
hiddenTextView.selectedRange = _hiddenTextViewDefaultRange; //horizontal and vertical center of the textview
[self.view addSubview:hiddenTextView];
[hiddenTextView becomeFirstResponder];
if (_keyboardShown)
[hiddenTextView resignFirstResponder];
I define the textViewDidChangeSelection as follows:
- (void)textViewDidChangeSelection:(UITextView *)textView {
NSLog(#"%lu",(unsigned long)textView.selectedRange.location);
if (textView.selectedRange.location != _hiddenTextViewDefaultRange.location)
{
hiddenTextView.selectedRange = _hiddenTextViewDefaultRange;
}
}
I set a 4 character text and put the selection index in position 2 (middle). The result is that if I press up arrow on the keyboard in simulator NSLog outputs 0 (start of text) and then 2 (reseting the position) which is correct. If I press up again it does the same thing so still correct. Problem is that if I hit up x times I have to hit down equal times before I am able to go to the end of the text (position 4).
I tried resetting the position with a UIButton instead of doing it programmatically and there it works fine. Any ideas?
I managed to overcome the problem. Instead of:
hiddenTextView.selectedRange = _hiddenTextViewDefaultRange;
I used:
dispatch_async(dispatch_get_main_queue(), ^{
textView.selectedRange = hiddenTextViewDefaultRange;
});
...to execute the command async. It worked, however I am not sure why it needed to be like that.
Related
This problem can be demonstrated by creating a new project in Xcode (I am using version 6.4) and using the following code:
#interface ViewController ()
#property (nonatomic, strong) UITextField * myTextField;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.myTextField = [[UITextField alloc] initWithFrame:CGRectMake(50, 50, self.view.frame.size.width-100, 50)];
self.myTextField.textAlignment = NSTextAlignmentRight;
[self.myTextField becomeFirstResponder];
self.myTextField.backgroundColor = [UIColor lightGrayColor]
self.myTextField.adjustsFontSizeToFitWidth = YES;
self.myTextField.font = [UIFont systemFontOfSize:40];
[self.view addSubview:self.myTextField];
}
- (void) viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
self.myTextField.text = #"1.5241578750119E18";
}
When running this project in iOS Simulator (iPhone 5 or 5S), the cursor is initially displayed after the last "1", and the "8" is not visible until a new character is typed.
This appears to be a bug by Apple, but my question is: is there a workaround for now that will force the text to right-align and show the cursor in the correct position?
To clarify the question further, the issue occurs when the text is set programmatically. I expect to see this:
But instead I am seeing this (note that the entire number is not visible and the cursor is showing after the "1" instead of the last digit which is an "8"):
This is a bug, existing in iOS, since iOS 7. Issue can be reproduced in stock applications like Settings as well. It affects text fields only when NSTextAlignmentRight is used. The original bug ID logged into Radar for this issue is 14485694. You may use centre or left text alignments, to circumvent this problem.
I would also suggest to file a new bug report to Apple,
Try this
UIView *paddingView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 5, 20)];
self.myTextField.leftView = paddingView;
self.myTextField.leftViewMode = UITextFieldViewModeAlways;
it will add some space to the left side of your TextField so that your cursor starts at correct position.
Hope it helps.
If I change your code to this:
- (void)viewDidLoad {
[super viewDidLoad];
self.myTextField = [[UITextField alloc] initWithFrame:CGRectMake(5, 50, self.view.frame.size.width-10, 50)];
self.myTextField.textAlignment = NSTextAlignmentRight;
[self.myTextField becomeFirstResponder];
self.myTextField.backgroundColor = [UIColor lightGrayColor];
self.myTextField.adjustsFontSizeToFitWidth = YES;
self.myTextField.font = [UIFont systemFontOfSize:60];
[self.view addSubview:self.myTextField];
}
And start typing in those numbers, the cursor ends up along the right edge of the text field just as it's supposed to.
Even when I start the app with the default value in the text, I see the cursor along the right edge of the text field fine.
Listening to UITextFieldTextDidChangeNotification notification instead of UIControlEventEditingChanged will fix the issue.
I met the same issue, try add dummy code like this
(void)textFieldDidBeginEditing:(UITextField *)textField {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
textField.text = textField.text;
});
}
I am using the following code with the label instantiated in a getter method:
-(UILabel *)switchUpLabel
{
if (!_switchUpLabel) _switchUpLabel = [[UILabel alloc]init];
return _switchUpLabel;
}
-(void)shouldShowSwitchupLabel:(BOOL)show
{
if (show)
{
self.switchUpLabel.text = #"🔀";
self.switchUpLabel.font = [UIFont systemFontOfSize:30.0];
[self.switchUpLabel sizeToFit];
self.switchUpLabel.frame = CGRectMake(self.imageView.center.x-self.switchUpLabel.frame.size.width/2, 10, self.switchUpLabel.frame.size.width, self.switchUpLabel.frame.size.height);
[self.view addSubview:self.switchUpLabel];
}
else [self.switchUpLabel removeFromSuperview];
}
When I execute [self.switchUpLabel removeFromSuperview]; the label remains on screen. The only way I can get the label "off" is to do something like
self.switchUpLabel.text = #"";
A few notes:
1/ I tried adding the remove statement immediately after the add statement which should have resulted in the label not showing at all. However, the label did show and was never removed.
2/ I verified that the label is not nil and is pointing to the same label in both cases (I output the label object to NSLog)
3/ I put the remove statement inside a main queue block to ensure it was executing on the main thread. No difference.
I am editing text in a UITextView. When the field opens for editing I need the cursor to be positioned after the final character of the existing text. The behavior I see is that the cursor is positioned more or less under the point where I touch the UITextView to start editing -- perhaps at the end of the word I touch on. I have tried setting the textview.selectedRange in both textViewDidBeginEditing: and textViewShouldBeginEditing: but that had no effect at all. I tried selecting ranges of the existing text, like {1,2} and that didn't do anything either. It seems like the selectedRange is more-or-less a read-only value?
- (void) textViewDidBeginEditing:(UITextView *)textView {
// Position the insertion cursor at the end of any existing text
NSRange insertionPoint = NSMakeRange([textView.text length], 0);
textView.selectedRange = insertionPoint;
}
How do I get the cursor to the end of the text?
The post referred to by the comment by artud2000 contains a working answer. To summarize here, add:
EDIT: The original answer was not sufficient. I've added toggling the editable property which seems to be sufficient. The problem is that the tap gesture only goes to the handler a single time (at most) and subsequent taps on the UITextField start editing directly. If it isn't editable then the UITextView's gesture recognizers are not active and the one I placed will work. This may well not be a good solution but it does seem to work.
- (void)viewDidLoad {
...
tapDescription = [[UITapGestureRecognizer alloc]
initWithTarget:self action:#selector(tapDescription:)];
[self.descriptionTextView addGestureRecognizer:tapDescription];
self.descriptionTextView.editable = NO; // if not set in storyboard
}
- (void) tapDescription:(UIGestureRecognizer *)gr {
self.descriptionTextView.editable = YES;
[self.descriptionTextView becomeFirstResponder];
}
- (void) textViewDidEndEditing:(UITextView *)textView {
//whatever else you need to do
textView.editable = NO;
}
The default position of the cursor seems to be after any existing text which solved my problem, but if you want to you can select the text in textViewDidBeginEditing: by setting the selectedRange -- for instance:
- (void) textViewDidBeginEditing:(UITextView *)textView {
// Example: to select the second and third characters when editing starts...
NSRange insertionPoint = NSMakeRange(1, 2);
textView.selectedRange = insertionPoint;
}
Thanks for the answer Charlie Price. I used it to solve my similar problem on a UITextView. Here's the answer in Swift in case anyone needs it:
...
let tapDescription = UITapGestureRecognizer(target: self, action: #selector(MyViewController.tapDescription(_:)))
self.descriptionTextView.addGestureRecognizer(tapDescription)
self.descriptionTextView.editable = false
...
func tapDescription(gr: UIGestureRecognizer) {
self.descriptionTextView.editable = true
self.descriptionTextView.becomeFirstResponder()
}
func textViewDidEndEditing(textView: UITextView) {
self.descriptionTextView.editable = false
}
In previous versions of iOS, my UITextView will scroll to the bottom using
[displayText scrollRangeToVisible:NSMakeRange(0,[displayText.text length])];
or
CGFloat topCorrect = displayText.contentSize.height -[displayText bounds].size.height;
topCorrect = (topCorrect<0.0?0.0:topCorrect);
displayText.contentOffset = (CGPoint){.x=0, .y=topCorrect};
But the former will now have the weird effect of starting at the top of a long length of text and animating the scroll to the bottom each time I append text to the view. Is there a way to pop down to the bottom of the text when I add text?
textView.scrollEnabled = NO;
[textView scrollRangeToVisible:NSMakeRange(textView.text.length - 1,0)];
textView.scrollEnabled = YES;
This really works for me in iOS 7.1.2.
For future travelers, building off of #mikeho's post, I found something that worked wonders for me, but is a bit simpler.
1) Be sure your UITextView's contentInsets are properly set & your textView is already firstResponder() before doing this.
2) After my the insets are ready to go, and the cursor is active, I call the following function:
private func scrollToCursorPosition() {
let caret = textView.caretRectForPosition(textView.selectedTextRange!.start)
let keyboardTopBorder = textView.bounds.size.height - keyboardHeight!
// Remember, the y-scale starts in the upper-left hand corner at "0", then gets
// larger as you go down the screen from top-to-bottom. Therefore, the caret.origin.y
// being larger than keyboardTopBorder indicates that the caret sits below the
// keyboardTopBorder, and the textView needs to scroll to the position.
if caret.origin.y > keyboardTopBorder {
textView.scrollRectToVisible(caret, animated: true)
}
}
I believe this is a bug in iOS 7. Toggling scrollEnabled on the UITextView seems to fix it:
[displayText scrollRangeToVisible:NSMakeRange(0,[displayText.text length])];
displayText.scrollEnabled = NO;
displayText.scrollEnabled = YES;
I think your parameters are reversed in NSMakeRange. Location is the first one, then how many you want to select (length).
NSMakeRange(0,[displayText.text length])
...would create a selection starting with the 0th (first?) character and going the entire length of the string. To scroll to the bottom you probably just want to select a single character at the end.
This is working for me in iOS SDK 7.1 with Xcdoe 5.1.1.
[textView scrollRangeToVisible:NSMakeRange(textView.text.length - 1,0)];
textView.scrollEnabled = NO;
textView.scrollEnabled = YES;
I do this as I add text programmatically, and the text views stays at the bottom like Terminal or command line output.
The best way is to set the bounds for the UITextView. It does not trigger scrolling and has an immediate effect of repositioning what is visible. You can do this by finding the location of the caret and then repositioning:
- (void)userInsertingNewText {
UITextView *textView;
// find out where the caret is located
CGRect caret = [textView caretRectForPosition:textView.selectedTextRange.start];
// there are insets that offset the text, so make sure we use that to determine the actual text height
UIEdgeInsets textInsets = textView.textContainerInset;
CGFloat textViewHeight = textView.frame.size.height - textInsets.top - textInsets.bottom;
// only set the offset if the caret is out of view
if (textViewHeight < caret.origin.y) {
[self repositionScrollView:textView newOffset:CGPointMake(0, caret.origin.y - textViewHeight)];
}
}
/**
This method allows for changing of the content offset for a UIScrollView without triggering the scrollViewDidScroll: delegate method.
*/
- (void)repositionScrollView:(UIScrollView *)scrollView newOffset:(CGPoint)offset {
CGRect scrollBounds = scrollView.bounds;
scrollBounds.origin = offset;
scrollView.bounds = scrollBounds;
}
I am trying to detect what formatting will be applied to new text being entered by the user. Meaning the cursor has a position, but no text selection (i.e. length = 0). Every time I try to query typingAttributes, even if it's just to log, the app crashes.
Thread 1:EXC_BAD_ACCESS (code=2, address=0x4)
In the Debug Navigator the last thing that happens is:
WebCore::Frame::styleAtSelectionStart() const
And just before that, I see
-[UITextView typingAttributes]
If I log with a selectedRange.length greater than 0, it seems to be fine. I've tried running with Zombies and Guard Malloc enabled, but am not seeing anything.
In my test project, the crash occurs as soon as the UITextView becomes firstResponder:
- (void)textViewDidChangeSelection:(UITextView *)textView
{
[self updateFormatButtons];
}
Which calls to check the attributes:
- (void)updateFormatButtons
{
UITextView *problemTextView = [self synopsisTextView];
NSRange selectedRange = [self getSelectedTextRange];
if (selectedRange.length == 0) // No text selected
{
// HELP - Why does the following line cause a crash?
// NSLog(#"textViewFormatting options: %#", [problemTextView typingAttributes]);
}
else
{
NSLog(#"Some text selected"); // Fine here
NSLog(#"textViewFormatting options: %#", [problemTextView typingAttributes]);
}
}
based on the selected range:
- (NSRange)getSelectedTextRange
{
NSRange rangeToReturn = NSMakeRange(NSNotFound, 0);
UITextView *textView = [self synopsisTextView];
if ([textView isFirstResponder])
{
rangeToReturn = [textView selectedRange];
}
return rangeToReturn;
}
Suggestions appreciated.
The issue appears to have been that when the text view’s selection changes, the contents of the typingAttributes dictionary are cleared automatically. So having the dictionary queried immediately was causing some kind of conflict. Putting a delay before calling seems to have addressed it:
[self performSelector:#selector(updateFormatButtons) withObject:nil afterDelay:0.15];