A lighter way of discovering text writing direction - ios

I want to determine the writing direction of a string so that I can render Right-to-Left languages such as Arabic correctly in a CALayer.
so I have this method
+(UITextAlignment)alignmentForString:(NSString *)astring
{
UITextView *text = [[UITextView alloc] initWithFrame:CGRectZero];
text.text = astring;
if ([text baseWritingDirectionForPosition:[text beginningOfDocument] inDirection:UITextStorageDirectionForward] == UITextWritingDirectionRightToLeft) {
return UITextAlignmentRight;
}
return UITextAlignmentLeft;
}
Works fine but feels a little heavy just for the purpose of discovering which way to align my text especially as its been called in drawInContext (although relatively infrequently).
Is there a lighter way of determining the writing direction for a given string or should I just stick with this under the basis of premature optimisation. And its got to be iOS 5 friendly.

The code in the question although functional is brutally expensive. Run it through a profiler and you will find that it spends close to 80% of the time in UITextView.setText when used in the drawInContext method for a layer.
Most of the answer is here in Detect Language of NSString
a better form is thus...
+(UITextAlignment)alignmentForString:(NSString *)astring
{
if (astring.length) {
NSArray *rightLeftLanguages = #[#"ar",#"he"];
NSString *lang = CFBridgingRelease(CFStringTokenizerCopyBestStringLanguage((CFStringRef)astring,CFRangeMake(0,[astring length])));
if ([rightLeftLanguages containsObject:lang]) {
return UITextAlignmentRight;
}
}
return UITextAlignmentLeft;
}
As Arabic and Hebrew are the only Right-to-Left languages detectable by CFStringTokenizerCopyBestStringLanguage and should also cover Persian, Urdu and Yiddish though I havent tested that.
see also http://en.wikipedia.org/wiki/Right-to-left

Swift
For me the .natural textAlignment of UITextView did not work unless I edited the text and line from start.
Based on the great answer to this question and the comment below it.
let text = "..."
let lang = CFStringTokenizerCopyBestStringLanguage(text as CFString, CFRange(location: 0, length: text.characters.count))
if let lang = lang {
let direction = NSLocale.characterDirection(forLanguage: lang as String)
if direction == .rightToLeft {
textView.textAlignment = .right
}
else {
textView.textAlignment = .left
}
}

Since UITextAlignment is deprecated, here's an NSString category with NSWritingDirection:
- (NSWritingDirection)alignment{
if (self.length) {
NSArray *rightLeftLanguages = #[#"ar",#"he"];
NSString *lang = CFBridgingRelease(CFStringTokenizerCopyBestStringLanguage((CFStringRef)self,CFRangeMake(0,self.length)));
if ([rightLeftLanguages containsObject:lang]) {
return NSWritingDirectionRightToLeft;
}
}
return NSWritingDirectionLeftToRight;
}

Related

How can I add different line spacing for different parts of NSAttributedString?

I want to have a single NSAttributedString contain multiple messages. If a single message has a long text and it wraps around, I want to have a line spacing of, say, 5. Because I have a single NSAttributedString contain multiple messages, I want to have a bigger line spacing between each message; let's say 20.
What I want
The 'I see' is one message. The 'I'd think it'd be both...' is one message, although it wraps down to two lines and 'Like a one way chat' is one message.
Notice how the line spacing between the 2nd and 3rd is smaller than the 1st and 2nd and between the 3rd and 4th.
What I've tried
I am appending a \n to the end of each message and I've tried using NSParagraphStyle which gives me control of the line spacing, but it seems to be all or nothing:
// index is the index of the group of messages as I iterate through them
// contentText is an NSMutableAttributedString
if index != messages.count - 1 {
let style = NSMutableParagraphStyle()
style.lineSpacing = 40.0
let lineReturn = NSMutableAttributedString(string: "\n")
contentText.appendAttributedString(lineReturn)
if index == 0 {
contentText.addAttribute(NSParagraphStyleAttributeName, value: style, range: NSMakeRange(contentText.length-lineReturn.length, lineReturn.length))
} else {
contentText.addAttribute(NSParagraphStyleAttributeName, value: style, range: NSMakeRange(contentText.length-lineReturn.length-1, lineReturn.length+1))
}
}
If I add the line spacing to the beginning it will set the line spacing for the entire label.
if index == 0 {
let style = NSMutableParagraphStyle()
style.lineSpacing = 40.0
contentText.addAttribute(NSParagraphStyleAttributeName, value: style1, range: NSMakeRange(start, 1))
}
(This is really only my latest try.)
Thanks for any help! :)
Details
Have very basic custom markup in your English message so you can
parse out the different pieces
Instruct your translators to leave the markup in and translate the
rest Have a UIView that can serve as the container of this message
Break your English message up in pieces to separate the regular text
from the clickable text
For each piece create a UILabel on the container UIView
For the clickable pieces, set your styling, allow user interaction
and create your tap gesture recognizer
Do some very basic bookkeeping to place the words perfectly across
the lines
For Understand.
In the view controller's viewDidLoad I placed this:
[self buildAgreeTextViewFromString:NSLocalizedString(#"I agree to the #<ts>terms of service# and #<pp>privacy policy#",
#"PLEASE NOTE: please translate \"terms of service\" and \"privacy policy\" as well, and leave the #<ts># and #<pp># around your translations just as in the English version of this message.")];
I'm calling a method that will build the message. Note the markup I came up with. You can of course invent your own, but key is that I also mark the ends of each clickable region because they span over multiple words.
Here's the method that puts the message together -- see below. First I break up the English message over the # character (or rather #"#" string). That way I get each piece for which I need to create a label separately. I loop over them and look for my basic markup of <ts> and <pp> to detect which pieces are links to what. If the chunk of text I'm working with is a link, then I style a bit and set up a tap gesture recogniser for it. I also strip out the markup characters of course. I think this is a really easy way to do it.
Note some subtleties like how I handle spaces: I simply take the spaces from the (localised) string. If there are no spaces (Chinese, Japanese), then there won't be spaces between the chunks either. If there are spaces, then those automatically space out the chunks as needed (e.g. for English). When I have to place a word at the start of a next line though, then I do need to make sure that I strip of any white space prefix from that text, because otherwise it doesn't align properly.
- (void)buildAgreeTextViewFromString:(NSString *)localizedString
{
// 1. Split the localized string on the # sign:
NSArray *localizedStringPieces = [localizedString componentsSeparatedByString:#"#"];
// 2. Loop through all the pieces:
NSUInteger msgChunkCount = localizedStringPieces ? localizedStringPieces.count : 0;
CGPoint wordLocation = CGPointMake(0.0, 0.0);
for (NSUInteger i = 0; i < msgChunkCount; i++)
{
NSString *chunk = [localizedStringPieces objectAtIndex:i];
if ([chunk isEqualToString:#""])
{
continue; // skip this loop if the chunk is empty
}
// 3. Determine what type of word this is:
BOOL isTermsOfServiceLink = [chunk hasPrefix:#"<ts>"];
BOOL isPrivacyPolicyLink = [chunk hasPrefix:#"<pp>"];
BOOL isLink = (BOOL)(isTermsOfServiceLink || isPrivacyPolicyLink);
// 4. Create label, styling dependent on whether it's a link:
UILabel *label = [[UILabel alloc] init];
label.font = [UIFont systemFontOfSize:15.0f];
label.text = chunk;
label.userInteractionEnabled = isLink;
if (isLink)
{
label.textColor = [UIColor colorWithRed:110/255.0f green:181/255.0f blue:229/255.0f alpha:1.0];
label.highlightedTextColor = [UIColor yellowColor];
// 5. Set tap gesture for this clickable text:
SEL selectorAction = isTermsOfServiceLink ? #selector(tapOnTermsOfServiceLink:) : #selector(tapOnPrivacyPolicyLink:);
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self
action:selectorAction];
[label addGestureRecognizer:tapGesture];
// Trim the markup characters from the label:
if (isTermsOfServiceLink)
label.text = [label.text stringByReplacingOccurrencesOfString:#"<ts>" withString:#""];
if (isPrivacyPolicyLink)
label.text = [label.text stringByReplacingOccurrencesOfString:#"<pp>" withString:#""];
}
else
{
label.textColor = [UIColor whiteColor];
}
// 6. Lay out the labels so it forms a complete sentence again:
// If this word doesn't fit at end of this line, then move it to the next
// line and make sure any leading spaces are stripped off so it aligns nicely:
[label sizeToFit];
if (self.agreeTextContainerView.frame.size.width < wordLocation.x + label.bounds.size.width)
{
wordLocation.x = 0.0; // move this word all the way to the left...
wordLocation.y += label.frame.size.height; // ...on the next line
// And trim of any leading white space:
NSRange startingWhiteSpaceRange = [label.text rangeOfString:#"^\\s*"
options:NSRegularExpressionSearch];
if (startingWhiteSpaceRange.location == 0)
{
label.text = [label.text stringByReplacingCharactersInRange:startingWhiteSpaceRange
withString:#""];
[label sizeToFit];
}
}
// Set the location for this label:
label.frame = CGRectMake(wordLocation.x,
wordLocation.y,
label.frame.size.width,
label.frame.size.height);
// Show this label:
[self.agreeTextContainerView addSubview:label];
// Update the horizontal position for the next word:
wordLocation.x += label.frame.size.width;
}
}
if you want to use gesture then use this method.
- (void)tapOnTermsOfServiceLink:(UITapGestureRecognizer *)tapGesture
{
if (tapGesture.state == UIGestureRecognizerStateEnded)
{
NSLog(#"User tapped on the Terms of Service link");
}
}
- (void)tapOnPrivacyPolicyLink:(UITapGestureRecognizer *)tapGesture
{
if (tapGesture.state == UIGestureRecognizerStateEnded)
{
NSLog(#"User tapped on the Privacy Policy link");
}
}
Hope this helps. I'm sure there are much smarter and more elegant ways to do this, but this is what I was able to come up with and it works nicely.
this answer display output like following screen shot...but you got idea from this answer.
Gotcha!
You need to play with baselineOffset attribute:
let contentText = NSMutableAttributedString(
string: "I see\nI'd think it`d be both a notification and a\nplace to see past announcements\nLike a one way chat.")
contentText.addAttribute(.baselineOffset, value: 10, range: NSRange(location: 0, length: 5))
contentText.addAttribute(.baselineOffset, value: -10, range: NSRange(location: 85, length: 20))
Result:
"I see
I'd think it`d be both a notification and a
place to see past announcements
Like a one way chat."

sizeWithAttributes() not giving me the right height after several break lines

Im using sizeWithAttributes() to get the size of a string with \n's in it. It works up to a number of 5 \n's and then it starts to return half a line too low on the height, so the last row gets cut in half (---).
Are there any other attributes than Font which will help me in my situation?
Code:
str = "text\ntext\ntext\ntext\ntext"
label = CATextLayer()
label. ...
let textSize = str.uppercaseString.sizeWithAttributes([NSFontAttributeName:UIFont(name: label.font as String, size: label.fontSize)!])
First, label.font might be not a string:
The font to use, currently may be either a CTFontRef, a CGFontRef, or a string naming the font
So you'd better perform some checks if you're not setting it in your code (I'll write my example in ObjC, but it should be pretty simple to translate it to Swift):
NSString* fontName;
CFTypeRef font = label.font;
if (CFGetTypeID(font) == CFStringGetTypeID())
{
fontName = (__bridge NSString *)(font);
}
else if (CFGetTypeID(font) == CGFontGetTypeID())
{
fontName = CFBridgingRelease(CGFontCopyFullName((CGFontRef)font));
}
else if (CFGetTypeID(font) == CTFontGetTypeID())
{
fontName = CFBridgingRelease(CTFontCopyFullName((CTFontRef)font));
}
Second, CATextLayer (unlike UILabel or string drawing functions) seems to use line height multiple different from 1 when drawing string content, so you need to use paragraph style to measure it correctly:
NSMutableParagraphStyle* style = [[NSMutableParagraphStyle alloc] init];
style.lineHeightMultiple = 1.05;
NSDictionary* attributes = #{
NSFontAttributeName: [UIFont fontWithName:fontName size:label.fontSize],
NSParagraphStyleAttributeName: style
};
CGSize textSize = [str sizeWithAttributes:attributes];
Multiplier 1.05 was actually chosen after some experiments, and it works pretty good for different font sizes from 8 to 72.

AutoLayout + RTL + UILabel text alignment

I'm finally getting round to wrestling with Auto Layout and can't seem to figure out how to get right-to-left (RTL) support to work the way I'd expect/want...
I have designed the view in Interface Builder as shown:
With the resulting app running as expected when using English:
However when switching to an RTL language (Arabic in this case), the entire view flips (which is great) but the UILabel's text is still left aligned. I'd expect it to be right aligned to keep it up against the UIImageView.
Clearly I'm missing something and/or this isn't covered by Auto Layout.
Am I supposed to set the textAlignment manually when using an RTL language?
You want NSTextAlignmentNatural. That infers the text alignment from the loaded application language (not from the script).
For iOS 9 and later (using Xcode 7), you can set this in the storyboard (choose the --- alignment option). If you need to target earlier releases, you'll need to create an outlet to the label and set the alignment in awakeFromNib.
- (void)awakeFromNib {
[[self label] setTextAlignment:NSTextAlignmentNatural];
}
For me those solutions didn't help, and I ended up doing something pretty ugly but it's the only one that did the trick for me. I added it as an NSString category:
NSString+Extras.h:
#import <Foundation/Foundation.h>
#interface NSString (Extras)
- (NSTextAlignment)naturalTextAligment;
#end
NSString+Extras.m:
#import "NSString+Extras.h"
#implementation NSString (Extras)
- (NSTextAlignment)naturalTextAligment {
NSArray *tagschemes = [NSArray arrayWithObjects:NSLinguisticTagSchemeLanguage, nil];
NSLinguisticTagger *tagger = [[NSLinguisticTagger alloc] initWithTagSchemes:tagschemes options:0];
[tagger setString:self];
NSString *language = [tagger tagAtIndex:0 scheme:NSLinguisticTagSchemeLanguage tokenRange:NULL sentenceRange:NULL];
if ([language rangeOfString:#"he"].location != NSNotFound || [language rangeOfString:#"ar"].location != NSNotFound) {
return NSTextAlignmentRight;
} else {
return NSTextAlignmentLeft;
}
}
#end
To detect the language I used this SO answer.
Follow up from Ken's answer
Setting textAlignment to NSTextAlignmentNatural is not possible on UILabel, it will result in an exception getting thrown:
Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: 'textAlignment does not accept NSTextAlignmentNatural'
It does work when using attributed text and this can be set in Interface Builder as shown:
However, it would appear that attributed text is not picked up when localising the storyboard.
To get around this I have left the UILabel configured as plain in Interface Builder and created an NSAttributedString with the label's text, set the alignment on the attributed string and assign it to the label's attributedText property:
-(void)viewDidLoad
{
[super viewDidLoad];
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
paragraphStyle.alignment = NSTextAlignmentNatural;
NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString:self.lbl.text];
[string addAttribute:NSParagraphStyleAttributeName
value:paragraphStyle
range:NSMakeRange(0, string.length)];
self.lbl.attributedText = string;
}
This works fine in this simple case but I can see it falling over when you need more complex attributed string styling. But obviously in that case you'd probably just be using NSLocalizedString or equivalents when creating the NSAttributedString.
#Aviel answer as a swift UILabel extension
//MARK: UILabel extension
extension UILabel {
func decideTextDirection () {
let tagScheme = [NSLinguisticTagSchemeLanguage]
let tagger = NSLinguisticTagger(tagSchemes: tagScheme, options: 0)
tagger.string = self.text
let lang = tagger.tagAtIndex(0, scheme: NSLinguisticTagSchemeLanguage,
tokenRange: nil, sentenceRange: nil)
if lang?.rangeOfString("he") != nil || lang?.rangeOfString("ar") != nil {
self.textAlignment = NSTextAlignment.Right
} else {
self.textAlignment = NSTextAlignment.Left
}
}
}
How to use it ?
label.text = "كتابة باللغة العربية" // Assign text
label.decideTextDirection() // Decide direction
I think you don't want to use text alignment in this case, for a label.
You can just let the width be determined by intrinsicContentSize, and remove any width constraints on the label. You will achieve the desired effect of the label text aligned to the view.
For x axis, you only need this constraint between label and imageview:
[imageview]-[label]
This is only a horizontal spacing constraint. No leading or trailing to superview.
#Aviel answer as a swift 4 UILabel extension
extension UILabel {
func decideTextDirection () {
let tagScheme = [NSLinguisticTagScheme.language]
let tagger = NSLinguisticTagger(tagSchemes: tagScheme, options: 0)
tagger.string = self.text
let lang = tagger.tag(at: 0, scheme: NSLinguisticTagScheme.language,
tokenRange: nil, sentenceRange: nil)
if lang?.rawValue.range(of: "he") != nil || lang?.rawValue.range(of: "ar") != nil {
self.textAlignment = NSTextAlignment.right
} else {
self.textAlignment = NSTextAlignment.left
}
}
}
usage
label.text = "كتابة باللغة العربية" // Assign text
label.decideTextDirection() // Decide direction
This function helped me out
-(void)fnForWritingDirection:(UILabel*)label textFor:(NSString *)stringForText{
NSMutableAttributedString* attrStr = [[NSMutableAttributedString alloc] initWithString: [stringForText stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
[paragraphStyle setBaseWritingDirection:NSWritingDirectionRightToLeft];
[attrStr addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, [attrStr length])];
label.attributedText=attrStr;
}
this worked for me:
extension UILabel {
func decideTextDirection () {
let tagScheme = [NSLinguisticTagScheme.language]
let tagger = NSLinguisticTagger(tagSchemes: tagScheme, options: 0)
tagger.string = self.text
let lang = tagger.tag(at: 0, scheme: NSLinguisticTagScheme.language, tokenRange: nil, sentenceRange: nil)
let langDir = NSLocale.characterDirection(forLanguage: lang?.rawValue ?? "en")
if langDir == .rightToLeft { self.textAlignment = NSTextAlignment.right }
else { self.textAlignment = NSTextAlignment.left }
}
}
usage:
nameLabel.text = "مهدی"
nameLabel.decideTextDirection()
You can use MyLinearLayout to easy support RTL and LRT.
Here is my version. It's simpler and also handles multiple languages in the source document.
The Main point is to use the dominantLanguage:
let lang = tagger.dominantLanguage
Code Snippet:
extension UILabel {
func determineTextDirection () {
guard self.text != nil else {return}
let tagger = NSLinguisticTagger(tagSchemes: [.language], options: 0)
tagger.string = self.text
let lang = tagger.dominantLanguage
let rtl = lang == "he" || lang == "ar"
self.textAlignment = rtl ? .right : .left
}
}
Usage:
titleLabel.text = "UILabel היפוך שפה עבור"
titleLabel.determineTextDirection()
Finally: Note that if the App is localized and you may rely on the phones language - the solution your after is:
"Natural Text Alignment for RTL":
i.e:
titleLabel.textAlignment = .natural
Use the NSLinguisticTagger when your app shows multiple lines with different languages. or when you allow free search in any language, regardless of the local.

UITextView get the current line

Is there a way (crazy hacks welcome) to get the current line as a string of a UITextView? This would include word wrapping, etc. For example, in this case:
The method would return "stack overflow. Isn't it great? I" because that is the current line based on the cursor.
It could also return "This is a test I made for" or "think so", based on the position of the cursor. I have tried working with both the UITextView methods and those of UITextInput protocol.
EDIT:
Here is the code I have attempted to use. The reason I need to find the string is to get it's length, so this is why you'll see UI based code.
NSRange location = self.textView.selectedRange;
NSString *searchString = [self.textView.text substringWithRange:NSMakeRange(0, location)];
CGSize currentStringDimensions = [searchString sizeWithFont:self.textView.font constrainedToSize:CGSizeMake(self.textView.frame.size.width, self.textView.frame.size.height) lineBreakMode:NSLineBreakByWordWrapping];
float numberOfRows = (currentStringDimensions.width/(self.textView.frame.size.width));
float left = (float)(numberOfRows - (int)numberOfRows) * (self.textView.frame.size.width);
This doesn't work, however. I think it might have something to with words being wrapped or the differently sized characters, but the left value is inconsistent or off after the first line.
The following code solution seem to be working. The "self" in this code refers to an instance of UITextView.
- (NSString *) getLineString:(NSRange)range
{
NSLayoutManager *manager = self.layoutManager;
// Convert it to a glyph range
NSRange matchingGlyphRange = [manager glyphRangeForCharacterRange:range actualCharacterRange:NULL];
// line fragment rect at location range
CGRect rect = [manager lineFragmentRectForGlyphAtIndex:matchingGlyphRange.location effectiveRange:nil];
// obtain the line range for the line fragment rect
NSRange lineRange = [manager glyphRangeForBoundingRect:rect inTextContainer:self.textContainer];
// extract the string out from lineRange
return [self.text substringWithRange:lineRange];
}
// ... later
NSLog(#"line %#", [self getLineString:self.selectedRange]);
This worked for me (self = the UITextView)
func getLineString() -> String {
return (self.text! as NSString).substringWithRange((self.text! as NSString).lineRangeForRange(self.selectedRange))
}
Swift 5 extension version of Gil's answer:
extension UITextView {
func getLineString() -> String {
guard let text = text else { return "" }
return (text as NSString).substring(with: (text as NSString).lineRange(for: self.selectedRange))
}
}
I ended up using the caretRect method of UITextInput to get the offset from the left. Worked flawlessly.

UITextField has trailing whitespace after secureTextEntry toggle

I have a button that toggles between Show/Hide mode (i.e. toggles a UITextField between secureTextEntry NO and YES). The purpose of which is to allow the user to see the password they are entering.
I followed the example (with the highest number of votes) here: UITextField secureTextEntry - works going from YES to NO, but changing back to YES has no effect
However, when I set secureTextEntry to NO, any text that was written there ends up with a space at the end. This does not seem to be a problem when setting secureTextEntry to YES.
For example, if I enter the text "mypassword" while setSecureTextEntry is set to NO, and then switch it to YES, the user will see ********** (10 dots), which is correct. If I setSecureTextEntry to NO, the user will see "mypassword " (with a space at the end, or at least, the cursor moved one space to the right).
Important note: In the debugger, the string value of text appears without the trailing space, like this:
(lldb) expr self.passwordText.text
(NSString *) $0 = 0x1d8450e0 #"mypassword"
I have tried trimming whitespace (per avoid middle whitespace in UITextField), but it has had no effect.
i've just encounter this case and finally solved this problem.
works on Latest iOS SDK, iOS 8.1
First of all, there is no trailing space at all.
The dot(shown in SecureEntry) character and normal character have different width and after you toggle isSecureEntry switch, the cursor didn't refresh it's position.
so i use this workaround to solved this problem.
- (void)toggle
{
NSString *tmpString;
[self.passwordTextField setSecureTextEntry:!self.passwordTextField.isSecureTextEntry];
if (self.passwordTextField.isSecureTextEntry) {
// do stuffs
} else {
// do other stuffs
}
// Workaround to refresh cursor
tmpString = self.passwordTextField.text;
self.passwordTextField.text = #" ";
self.passwordTextField.text = tmpString;
}
Swift 3+
// Workaround to refresh cursor
let currentText: String = self.userPassword.text!
self.userPassword.text = "";
self.userPassword.text = currentText
hope it helps!
PRE-iOS-8.0 (dated solution)... In your button's action method (toggling between secureTextEntry YES and NO), simply set UITextField's text property to its current text value. Although this may seem redundant and a bit like a hack, this will redraw the cursor in the right position. Here's an example of what your button's action method should look like now...
-(void)toggleButtonPressed:(UIButton*)sender
{
// Toggle between secure and not-so-secure entry
self.toggleButton.selected = !self.toggleButton.selected;
self.textfield.secureTextEntry = !self.toggleButton.selected;
// * Redundant (but necessary) line *
[self.textfield setText:self.textfield.text];
}
POST-iOS-8.0... As of iOS 8.0, it appears that UITextField's text setter no longer redraws the cursor when called with a string equal to its current string value. Now, we need to take this a step further and actually change the text value before resetting it again. Replace the above setText: line with something like these lines.
// * Extra redundant (but necessary) lines *
NSString *currentText = self.textfield.text;
[self.textfield setText:#"Arbitrary string..."]; // Change the value
[self.textfield setText:currentText]; // Reset the value
I have a clean solution not going dirty with text property of UITextField.
Wrap them in this style.
[self.passwordTextField resignFirstResponder]; // first resign its first responder.
// change `secureTextEntry` property's value if necessary.
if (self.passwordTextField.secureTextEntry) {
self.passwordTextField.secureTextEntry = NO;
self.passwordEcryptButton.selected = YES;
}else{
self.passwordTextField.secureTextEntry = YES;
self.passwordEcryptButton.selected = NO;
}
[self.passwordTextField becomeFirstResponder]; // finally gain its first responder again.
In order to work around this bug in iOS you can simply do the following (works for any iOS version):
- (IBAction)toggleSecureTextEntry:(UIButton *)button
{
self.textField.secureTextEntry = !self.textField.secureTextEntry;
NSString *originalText = self.textField.text;
self.textField.text = nil;
self.textField.text = originalText;
}
You can fix it like this:
NSString *currentText = self.textfield.text;
self.textfield.text = #"";
self.textfield.text = currentText;
This work for me on iOS 8
if (self.passwordTextField.secureTextEntry) {
// Display password and keep selected text range
UITextRange *selectedTextRange = self.passwordTextField.selectedTextRange;
NSString *password = self.passwordTextField.text;
self.passwordTextField.secureTextEntry = NO;
self.passwordTextField.text = [#"" stringByPaddingToLength:password.length withString:#" " startingAtIndex:0]; // Done for carret redrawing
self.passwordTextField.text = password;
self.passwordTextField.selectedTextRange = selectedTextRange;
}
else {
// Hide password and keep selected text range
UITextRange *selectedTextRange = self.passwordTextField.selectedTextRange;
NSString *password = self.passwordTextField.text;
self.passwordTextField.secureTextEntry = YES;
self.passwordTextField.text = [#"" stringByPaddingToLength:password.length withString:#" " startingAtIndex:0]; // Done for carret redrawing
self.passwordTextField.text = password;
self.passwordTextField.selectedTextRange = selectedTextRange;
}
UITextPosition *beginning = [self.passwordField beginningOfDocument];
[self.passwordField setSelectedTextRange:[self.passwordField textRangeFromPosition:beginning
toPosition:beginning]];
UITextPosition *end = [self.passwordField endOfDocument];
[self.passwordField setSelectedTextRange:[self.passwordField textRangeFromPosition:end
toPosition:end]];
This is what I used for iOS 8
When we change a textfield.secureTextEntry property, the caret position is not updated. To fix this, the code below used to work before IOS 8:
pwdTextField.text = pwdTextField.text
Now it doesn't. It seems IOS 8 detects the new value equals old value and does nothing. So to make it work again we have to actually change the value. Here is the swift version that works for me.
let str = pwdTextField.text
pwdTextField.text = str + " "
pwdTextField.text = str
This is another possibility to solve this issue, where self.passwordText is the UITextField:
if (self.passwordText.isFirstResponder) {
[self.passwordText resignFirstResponder];
[self.passwordText becomeFirstResponder];
}
It appears that the second solution in the referenced link, when implemented, has the desired behavior of not adding an extra space:
https://stackoverflow.com/a/8495888/738190
This Works in my case
BOOL wasFirstResponder = [self.passwordTextField isFirstResponder];
if([self.passwordTextField isSecureTextEntry])
{
//This three lines are key
self.passwordTextField.delegate = nil;
[self.passwordTextField resignFirstResponder];
self.passwordTextField.delegate = self;
}
[self.passwordTextField setSecureTextEntry: !self.passwordTextField.isSecureTextEntry];
if(wasFirstResponder)
[self.passwordTextField becomeFirstResponder];
Swift UITextField extension:
extension UITextField {
func toggleSecureEntry() {
let wasFirstResponder = isFirstResponder
if wasFirstResponder { resignFirstResponder() }
isSecureTextEntry.toggle()
if wasFirstResponder { becomeFirstResponder() }
}
}
Setting textField.text solution also works in some situations but not for my need (Custom font with two text fields. Caused font changes and glitches on runtime.) Adding here too.
func toggleSecureEntry() {
isSecureTextEntry.toggle()
let originalText = text
text = nil
text = originalText
}
To get the cursor to reposition correctly, setting the font attributes seemed to do the trick for me.
// Hack to update cursor position
self.passwordTf.defaultTextAttributes = #{NSFontAttributeName: textFieldFont, NSForegroundColorAttributeName: textFieldColor};
// Change secure entry
self.passwordTf.secureTextEntry = !self.passwordTf.secureTextEntry;
Tested on iOS8, iOS9.
Hope it helps!
Everytime the text is set in the UITextField, the cursor postition is updated
So I used this code
partial void btnShowPassword_ToutchUpInside (UIButton sender)
{
if (TxtPassword.SecureTextEntry == true) {
TxtPassword.SecureTextEntry = false;
TxtPassword.Text = TxtPassword.Text;
} else {
TxtPassword.SecureTextEntry = true;
}
}
Here is the solution:
- (void)showHidePassword:(UIButton *)sender {
EDSignUpCell *cell = [self.signUpTblView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:3 inSection:0]];
if(!TRIM_SPACE(cell.cellTextField.text).length) {return;}
[cell.showHidePasswordBtn setSelected:!cell.showHidePasswordBtn.isSelected];
cell.cellTextField.secureTextEntry = cell.showHidePasswordBtn.isSelected;
[cell.cellTextField setText:cell.cellTextField.text];
[cell.cellTextField becomeFirstResponder];
}
I'm using this, Works fine.
[self.yourTextField becomeFirstResponder];
Swift 4
Bug is on radar, there is explanation of workaround also: http://www.openradar.me/38465011
Here is cut of temporary workaround how to natively update caret (cursor) position.
// update caret position
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
let (beginning, end) = (self.beginningOfDocument, self.endOfDocument)
self.selectedTextRange = self.textRange(from: beginning, to: end)
self.selectedTextRange = self.textRange(from: end, to: end)
}
I had a similar issue and realized it was because I was updating the text before setting the secureTextEntry property. It makes sense that the textField would draw out the caret at the location it'd be at if it were using secureTextEntry.
I did not read the entire problem nor did I visit the solution linked by OP, but in case someone else has the same issue as me:
Try updating your text after setting the secureTextEntry property.

Resources