How to achieve something like NSLineBreakByTruncatingHead for UITextField? - ios

I need to achieve something exactly like NSLineBreakByTruncatingHead for UITextField as shown here. Let's assume the original text is:
This is the long text that cannot be shown inside a UITextField
I need it like:
...cannot be shown inside a UITextField
but currently I am getting something like:
This is the long text that cannot...
simply the truncation at the beginning. The lineBreakMode property is not given for UITextField. How can I achieve it?

I took the solution here and modified it to truncate the head of a string instead of the tail. Know that it only shows the ellipsis when the field is not being edited.
NOTE: This solution is for iOS 7+ only. To use in iOS 6, use sizeWithFont: instead of sizeWithAttributes: in the NSString+TruncateToWidth.m file.
EDIT: Added support for iOS 6
NSString+TruncateToWidth.h
#interface NSString (TruncateToWidth)
- (NSString*)stringByTruncatingToWidth:(CGFloat)width withFont:(UIFont *)font;
#end
NSString+TruncateToWidth.m
#import "NSString+TruncateToWidth.h"
#define ellipsis #"…"
#implementation NSString (TruncateToWidth)
- (NSString*)stringByTruncatingToWidth:(CGFloat)width withFont:(UIFont *)font
{
// Create copy that will be the returned result
NSMutableString *truncatedString = [self mutableCopy];
// Make sure string is longer than requested width
if ([self widthWithFont:font] > width)
{
// Accommodate for ellipsis we'll tack on the beginning
width -= [ellipsis widthWithFont:font];
// Get range for first character in string
NSRange range = {0, 1};
// Loop, deleting characters until string fits within width
while ([truncatedString widthWithFont:font] > width)
{
// Delete character at beginning
[truncatedString deleteCharactersInRange:range];
}
// Append ellipsis
[truncatedString replaceCharactersInRange:NSMakeRange(0, 0) withString:ellipsis];
}
return truncatedString;
}
- (CGFloat)widthWithFont:(UIFont *)font
{
if([self respondsToSelector:#selector(sizeWithAttributes:)])
return [self sizeWithAttributes:#{NSFontAttributeName:font}].width;
return [self sizeWithFont:font].width;
}
Using it:
...
// Make sure to import the header file where you want to use it
// assumes instance variable holds your string that populates the field
fieldString = #"abcdefghijklmnopqrstuvwxyz1234567890";
// Size will need to be less than text field's width to account for padding
_myTextField.text = [fieldString stringByTruncatingToWidth:(_myTextField.frame.size.width - 15) withFont:_myTextField.font];
...
// use textFieldShouldBeginEditing to make it animate from the start of the field to the end of the string if you prefer that. I found it a little distracting
- (void)textFieldDidBeginEditing:(UITextField *)textField
{
textField.text = fieldString;
}
- (BOOL)textFieldShouldEndEditing:(UITextField *)textField
{
fieldString = textField.text;
textField.text = [textField.text stringByTruncatingToWidth:(textField.frame.size.width - 15) withFont:textField.font];
return YES;
}

I had a similar requirement, I wrote a Swift version of #Stonz2 solution, it worked most of the times, havent used in production yet as the requirement was removed later... anyways posting it here
extension String {
func stringByTruncatingLeadingForWidth(width: CGFloat, withFont font: UIFont) -> String{
var modifiedString = self
var mutableWidth = width
let ellipsis = "..."
if (self.widthOfString(usingFont: font) > width) {
let ellipsisWidth = ellipsis.widthOfString(usingFont: font)
// else this will go for infinite loop...mutable width will go -ve
if mutableWidth > ellipsisWidth {
mutableWidth -= ellipsis.widthOfString(usingFont: font)
}
let range = NSMakeRange(0, 1)
while modifiedString.widthOfString(usingFont: font) > mutableWidth {
modifiedString.deleteCharactersInRange(range: range)
print(modifiedString)
print(mutableWidth)
}
guard let swiftRange = Range(NSMakeRange(0, 3), in: modifiedString) else { return "" }
modifiedString.replaceSubrange(swiftRange, with: [".",".","."])
}
return modifiedString
}
func widthOfString(usingFont font: UIFont) -> CGFloat {
let fontAttributes = [NSAttributedString.Key.font: font]
let size = self.size(withAttributes: fontAttributes)
return size.width
}
mutating func deleteCharactersInRange(range: NSRange) {
guard let swiftRange = Range(range, in: self) else { return }
self.removeSubrange(swiftRange)
}
}
var str1 = "Hello how are you"
let newStr = str1.stringByTruncatingLeadingForWidth(width: 100, withFont: .systemFont(ofSize: 15))

Related

UITextView exclusionPaths bottom right

I am trying to do something whatsapp like for messages for having a cell that resizes based on contentsize and in the bottom right corner to have the date... the problem is that I don't know the rectangle position for the excluded path because the text is dynamic and I have something like this but works only for multiple lines, but not for a single line:
NSString *txt=#"I'm writing ";//a text renderer using Core Text, and I discovered I’ll need to wrap text around objects (such as is done in any DTP program). I couldn’t find any easy answers as to how to do this in the documentation, so having finally got it working I’ll share what I did. To lay out text in a custom shape in Core Text, you can pass a CGPath in when you create your CTFramesetterRef. Originally this only supported rectangular paths, but now it supports fully custom paths. My first thought was to see if I could subtract the region to wrap around from the path for my frame’s border, and pass the result in to Core Text as a path. It turns out firstly that subtracting one path from another in Core Graphics is not trivial. However, if you simply add two shapes to the same path, Core Graphics can use a winding rule to work out which areas to draw. Core Text, at least as of iOS 4.2, can also use this kind of algorithm. This will work for many cases: if you can guarantee that your object to be wrapped will be fully inside the frame (and not overlapping the edge), just go ahead and add its border to the same path as your frame, and Core"; // Text will do the rest.";
txtText.text=txt;
CGSize textFrame = [txtText sizeThatFits:CGSizeMake(235, MAXFLOAT)];
UIBezierPath * imgRect = [UIBezierPath bezierPathWithRect:CGRectMake(textFrame.width-35, textFrame.height-20, 35, 20)];
txtText.textContainer.exclusionPaths = #[imgRect];
textFrame = [txtText sizeThatFits:CGSizeMake(235, MAXFLOAT)];
//int frameWidth=375;
txtText.frame=CGRectIntegral(CGRectMake(10, 10, textFrame.width, textFrame.height));
lblHour.frame=CGRectIntegral(CGRectMake(textFrame.width-35, textFrame.height-15, 35, 20));
viewCanvasAround.frame=CGRectIntegral(CGRectMake(50, 100, textFrame.width+20, textFrame.height+20));
I had the same problem too. I added attributedString with textAttachment which it has a size. You will set an estimated maximum size. There's no problem with a single line either.
(Sorry, I am not good at English.)
This is the sample code.
var text: String! {
didSet {
let font = UIFont.systemFont(ofSize: 13);
let att = NSMutableAttributedString.init(string: text, attributes: [NSAttributedString.Key.foregroundColor : UIColor.black,
NSAttributedString.Key.font : font]);
let attachment = NSTextAttachment.init(data: nil, ofType: nil);
var size = extraSize;
size.height = font.lineHeight;
attachment.bounds = CGRect.init(origin: CGPoint.init(), size: size);
att.append(NSAttributedString.init(attachment: attachment));
textView.attributedText = att;
}
}
There is a nice solution my colleague found in actor.im. https://github.com/actorapp/actor-platform/blob/master/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleTextCell.swift#L312
let container = YYTextContainer(size: CGSizeMake(maxTextWidth, CGFloat.max))
textLayout = YYTextLayout(container: container, text: attributedText)!
// print("Text Layouted")
// Measuring text and padded text heights
let textSize = textLayout.textBoundingSize
if textLayout.lines.count == 1 {
if textLayout.textBoundingSize.width < maxTextWidth - timeWidth {
//
// <line_0> <date>
//
bubbleSize = CGSize(width: textSize.width + timeWidth, height: textSize.height)
} else {
//
// <line_________0>
// <date>
//
bubbleSize = CGSize(width: textSize.width, height: textSize.height + 16)
}
} else {
let maxWidth = textSize.width
let lastLine = textLayout.lines.last!.width
if lastLine + timeWidth < maxWidth {
//
// <line_________0>
// <line_________1>
// ..
// <line_n> <date>
//
bubbleSize = textSize
} else if lastLine + timeWidth < maxTextWidth {
//
// |------------------|
// <line______0>
// <line______1>
// ..
// <line______n> <date>
//
bubbleSize = CGSize(width: max(lastLine + timeWidth, maxWidth), height: textSize.height)
} else {
//
// <line_________0>
// <line_________1>
// ..
// <line_________n>
// <date>
//
bubbleSize = CGSize(width: max(timeWidth, maxWidth), height: textSize.height + 16)
}
}
I had the same problem (for me it was a button in right corner) and there is the way how I resolved it:
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
[self updateTextExclusionPaths];
}
- (void)updateTextExclusionPaths {
// Remove old exclusionPaths to calculate right rects of new
textView.textContainer.exclusionPaths = #[];
// Force textView to layout the text without exclusionPaths
[textView.layoutManager glyphRangeForTextContainer:textView.textContainer];
/// Because of some specifics of my implementation I had a problem with wrong frame of my button. So I calculated it manually
// Calculate new exclusionPaths
CGRect buttonFrameInTextView = [superViewOfTextView convertRect:button.frame toView:textView];
// Calculate textView.text size without any exclusionPaths to calculate right button frame
CGSize textViewSize = [textView sizeThatFits:CGSizeMake(textView.frame.size.width, CGFLOAT_MAX)];
// Update Y position of button
buttonFrameInTextView.origin.y = size.height - buttonFrameInTextView.size.height;
// Add new exclusionPaths
UIBezierPath *buttonRectBezierPath = [UIBezierPath bezierPathWithRect:buttonFrameInTextView];
textView.textContainer.exclusionPaths = #[ buttonRectBezierPath ];
}

Limit the number of line in UITextView

Question
Is there any way to "accurately" limit the number of line in UITextView for target iOS 5.0?
What I had tried
As I had search in stack overflow. I had found these question been ask before in links below.
In UITextView, how to get the point that next input will begin another line
Limit the number of lines for UITextview
Limit number of lines in UITextView
But I still can't get the accurate number of line in UITextView when I tried to decide whether to return YES or NO in textView:shouldChangeTextInRange:replacementText:.
I had tried used the code which is the answer of Limiting text in a UITextView and the code after modified (remove -15 in the answer) is showing below.
- (BOOL)textView:(UITextView *)aTextView shouldChangeTextInRange:(NSRange)aRange replacementText:(NSString*)aText
{
NSString* newText = [aTextView.text stringByReplacingCharactersInRange:aRange withString:aText];
// TODO - find out why the size of the string is smaller than the actual width, so that you get extra, wrapped characters unless you take something off
CGSize tallerSize = CGSizeMake(aTextView.frame.size.width,aTextView.frame.size.height*2); // pretend there's more vertical space to get that extra line to check on
CGSize newSize = [newText sizeWithFont:aTextView.font constrainedToSize:tallerSize lineBreakMode:UILineBreakModeWordWrap];
if (newSize.height > aTextView.frame.size.height)
{
[myAppDelegate beep];
return NO;
}
else
return YES;
}
I also figure out a way to get the number of line in UITextView. The way is to calculate by contentSize property like textView.contenSize.height/font.lineHeight. This method can get the accurate number of lines in UITextView. But the problem is that contentSize get in textView:shouldChangeTextInRange:replacementText: and textViewDidChange: is the old contentSize. So I still can't limit the number of lines in UITextView.
Solution I used
This is kind of workaround but at least it work.
Step 1
At first you need to create a temporary new UITextView with all the same as the original UITextView but setting the temporary UITextView hidden in .xib file. In this sample code I name the temporary UITextView as tempTextInputView
Step 2
Add new referencing outlet to .h file like
#property (retain, nonatomic) IBOutlet UITextView *tempTextInputView;// Use to calculate the number of lines in UITextView with new text
Step 3
Add code below.
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text{
NSString *newText = [textView.text stringByReplacingCharactersInRange:range withString:text];
_tempTextInputView.text = newText;
// Calcualte the number of lines with new text in temporary UITextView
CGRect endRectWithNewText = [_tempTextInputView caretRectForPosition:_tempTextInputView.endOfDocument];
CGRect beginRectWithNewText = [_tempTextInputView caretRectForPosition:_tempTextInputView.beginningOfDocument];
float beginOriginY = beginRectWithNewText.origin.y;
float endOriginY = endRectWithNewText.origin.y;
int numberOfLines = (endOriginY - beginOriginY)/textView.font.lineHeight + 1;
if (numberOfLines > maxLinesInTextView) {// Too many lines
return NO;
}else{// Number of lines will not over the limit
return YES;
}
}
Discussion
maxLinesInTextView is an int variable represent the maximum number of lines you want.
I use a temporary UITextView to setting new text is because when I setting the new text simply in the original UITextView, I got some problem when I typing in ChuYin(注音) keyboard which is a Traditional Chinese input method.
I still using textView:shouldChangeTextInRange:replacementText: but not textViewDidChange: is because I got some problem when cache the text before modify with a global NSString and replace the UITextView.text with that global NSString in textViewDidChange:.
Here's how you can use the UITextViewDelegate shouldChangeTextInRange: method to limit the text entry to the height of the text view:
func textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool {
// Combine the new text with the old
let combinedText = (textView.text as NSString).stringByReplacingCharactersInRange(range, withString: text)
// Create attributed version of the text
let attributedText = NSMutableAttributedString(string: combinedText)
attributedText.addAttribute(NSFontAttributeName, value: textView.font, range: NSMakeRange(0, attributedText.length))
// Get the padding of the text container
let padding = textView.textContainer.lineFragmentPadding
// Create a bounding rect size by subtracting the padding
// from both sides and allowing for unlimited length
let boundingSize = CGSizeMake(textView.frame.size.width - padding * 2, CGFloat.max)
// Get the bounding rect of the attributed text in the
// given frame
let boundingRect = attributedText.boundingRectWithSize(boundingSize, options: NSStringDrawingOptions.UsesLineFragmentOrigin, context: nil)
// Compare the boundingRect plus the top and bottom padding
// to the text view height; if the new bounding height would be
// less than or equal to the text view height, append the text
if (boundingRect.size.height + padding * 2 <= textView.frame.size.height){
return true
}
else {
return false
}
}
As I have mentioned in my answer here, I advise against using shouldChangeCharactersInRange: since it is invoked before the text is actually changed.
Using the textViewDidChangeMethod: makes more sense, since it is invoked after the text actually changes. From there you can easily decide what to do next.
One options is to modify the textView yourself in the shouldChangeTextInRange delegate method and always return NO (because you already did the work for it).
- (BOOL)textView:(UITextView *)aTextView shouldChangeTextInRange:(NSRange)aRange replacementText:(NSString*)aText
{
NSString* oldText = aTextView.text;
NSString* newText = [aTextView.text stringByReplacingCharactersInRange:aRange withString:aText];
aTextView.text = newText;
if(/*aTextView contentSize check herer for number of lines*/)
{
//If it's now too big
aTextView.text = oldText;
}
return NO
}

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.

How to truncate the title of a tabbarcontroller item

I'm creating tabbaritems dynamically, and sometimes the title of the item exceeds the item's space and it take the space of the next tabbaritem.
Somebody knows how to prevent it? How to truncate the name?
Sorry, but I can't post photos yet.
Thanks in advance!
Actually there is no easy way to do it.
You can truncate NSString to some defined width( in ex. "TestBarTitle"->"TestB.." ) before setting it as a title:
- (NSString*)stringByTruncatingStringWithFont:(UIFont *)font forWidth:(CGFloat)width lineBreakMode:(UILineBreakMode)lineBreakMode {
NSMutableString *resultString = [[self mutableCopy] autorelease];
NSRange range = {resultString.length-1, 1};
while ([resultString sizeWithFont:font forWidth:FLT_MAX lineBreakMode:lineBreakMode].width > width) {
// delete the last character
[resultString deleteCharactersInRange:range];
range.location--;
// replace the last but one character with an ellipsis
[resultString replaceCharactersInRange:range withString:truncateReplacementString];
}
return resultString;
}
Or you can manually implement UITabBar ( UIImageView + UIButtons and UILabels ), so you will have 100% control of this UI element;
Solution for Swift 4.2:
var resultString = title
var attributedText = NSAttributedString(string: resultString, attributes: [NSAttributedStringKey.font: font])
while attributedText.boundingRect(with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: 15), options: .usesLineFragmentOrigin, context: nil).size.width > availableWidth {
// delete last character
resultString.removeLast(2)
resultString.append(".")
attributedText = NSAttributedString(string: resultString, attributes: [NSAttributedStringKey.font: font])
}

how to move cursor in UITextField after setting its value

can anybody help me with this: i need to implement UITextField for input number. This number should always be in decimal format with 4 places e.g. 12.3456 or 12.3400.
So I created NSNumberFormatter that helps me with decimal places.
I am setting the UITextField value in
-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
method.
I proccess input, use formatter and finally call [textField setText: myFormattedValue];
This works fine but this call also moves the cursor to the end of my field. That is unwanted. E.g. I have 12.3400 in my field and the cursor is located on the very beginning and user types number 1. The result value is 112.3400 but cursor is moved at the end. I want to end with cursor when the user expects (just after the number 1 recently added). There are some topics about setting cursor in TextView but this is UITextField. I also tried to catch selectedTextRange of the field, which saves the cursor position properly but after setText method call, this automatically changes and the origin UITextRange is lost (changed to current). hopefully my explanation is clear.
Please, help me with this. thank you very much.
EDIT : Finally, i decided to switch the functionality to changing the format after whole editing and works good enough. I have done it by adding a selector forControlEvents:UIControlEventEditingDidEnd.
After the UITextField.text property is changed, any previous references to UITextPosition or UITextRange objects that were associated with the old text will be set to nil after you set the text property. You need to store what the text offset will be after the manipulation will be BEFORE you set the text property.
This worked for me (note, you do have to test whether cursorOffset is < textField.text.length if you remove any characters from t in the example below):
- (BOOL) textField:(UITextField *) textField shouldChangeCharactersInRange:(NSRange) range replacementString:(NSString *) string
{
UITextPosition *beginning = textField.beginningOfDocument;
UITextPosition *start = [textField positionFromPosition:beginning offset:range.location];
UITextPosition *end = [textField positionFromPosition:start offset:range.length];
UITextRange *textRange = [textField textRangeFromPosition:start toPosition:end];
// this will be the new cursor location after insert/paste/typing
NSInteger cursorOffset = [textField offsetFromPosition:beginning toPosition:start] + string.length;
// now apply the text changes that were typed or pasted in to the text field
[textField replaceRange:textRange withText:string];
// now go modify the text in interesting ways doing our post processing of what was typed...
NSMutableString *t = [textField.text mutableCopy];
t = [t upperCaseString];
// ... etc
// now update the text field and reposition the cursor afterwards
textField.text = t;
UITextPosition *newCursorPosition = [textField positionFromPosition:textField.beginningOfDocument offset:cursorOffset];
UITextRange *newSelectedRange = [textField textRangeFromPosition:newCursorPosition toPosition:newCursorPosition];
[textField setSelectedTextRange:newSelectedRange];
return NO;
}
And here is the swift version :
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let beginning = textField.beginningOfDocument
let start = textField.positionFromPosition(beginning, offset:range.location)
let end = textField.positionFromPosition(start!, offset:range.length)
let textRange = textField.textRangeFromPosition(start!, toPosition:end!)
let cursorOffset = textField.offsetFromPosition(beginning, toPosition:start!) + string.characters.count
// just used same text, use whatever you want :)
textField.text = (textField.text! as NSString).stringByReplacingCharactersInRange(range, withString: string)
let newCursorPosition = textField.positionFromPosition(textField.beginningOfDocument, offset:cursorOffset)
let newSelectedRange = textField.textRangeFromPosition(newCursorPosition!, toPosition:newCursorPosition!)
textField.selectedTextRange = newSelectedRange
return false
}
Here is a swift 3 version
extension UITextField {
func setCursor(position: Int) {
let position = self.position(from: beginningOfDocument, offset: position)!
selectedTextRange = textRange(from: position, to: position)
}
}
What actually worked for me was very simple, just used dispatch main async:
DispatchQueue.main.async(execute: {
textField.selectedTextRange = textField.textRange(from: start, to: end)
})
Implement the code described in this answer: Moving the cursor to the beginning of UITextField
NSRange beginningRange = NSMakeRange(0, 0);
NSRange currentRange = [textField selectedRange];
if(!NSEqualRanges(beginningRange, currentRange))
{
[textField setSelectedRange:beginningRange];
}
EDIT: From this answer, it looks like you can just use this code with your UITextField if you're using iOS 5 or above. Otherwise, you need to use a UITextView instead.
Swift X.
To prevent from moving cursor from last position.
func textFieldDidChangeSelection(_ textField: UITextField) {
let point = CGPoint(x: bounds.maxX, y: bounds.height / 2)
if let textPosition = textField.closestPosition(to: point) {
textField.selectedTextRange = textField.textRange(from: textPosition, to: textPosition)
}
}

Resources