I have the following code and want to make parts of my text be clickable and call another UIViewController (not a website).
NSMutableAttributedString *str = [[NSMutableAttributedString alloc] initWithString:#"testing it out #clickhere"];
NSInteger length = str.length;
[str addAttribute:NSForegroundColorAttributeName value:[UIColor bestTextColor] range:NSMakeRange(0,length)];
The NSMutableAttributedString gets set to a UILabel like so:
label.attributedText = str;
Whats the best way to do this? I can't seem to find a great answer.
An example of what I want is suppose I have a UILabel like so with the following text:
This is my label. Click here to go to UIViewController1 and then go to UIViewController1 by this #tag.
I want the text "here" to be passed for the first click event and the word "#tag" to be passed to the same click event.
What if you used the value field to pass in the destination?
[attributedString addAttribute:NSLinkAttributeName
value:[#"destinationController1" stringByAppendingString:username]
range:range];
Then override the delegate method:
- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange
{
if ([URL.scheme isEqualToString:#"destinationController1"]) {
// Launch View controller
return NO;
}
return YES;
}
My solution requires the use of a UITextView (which is significantly easier, and I urge that you use it instead).
Swift
class ViewController: UIViewController {
#IBOutlet weak var textView:UITextView!;
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let gestureRecognizer = UITapGestureRecognizer(target: self, action: "textViewTapped:");
gestureRecognizer.numberOfTapsRequired = 1;
gestureRecognizer.numberOfTouchesRequired = 1;
self.textView.addGestureRecognizer(gestureRecognizer);
}
func textViewTapped(sender: UITapGestureRecognizer) {
let wordTarget = "here";
let word = UITextView.getWordAtPosition(sender.locationInView(self.textView), textView: self.textView);
if word == wordTarget {
let plainString = self.textView.attributedText.string;
let substrings = NSMutableArray();
let scanner = NSScanner(string: plainString);
scanner.scanUpToString("#", intoString: nil);
while !scanner.atEnd {
var substring:NSString? = nil;
scanner.scanString("#", intoString: nil);
let space = " ";
if scanner.scanUpToString(space, intoString: &substring) {
// If the space immediately followed the #, this will be skipped
substrings.addObject(substring!);
}
scanner.scanUpToString("#", intoString: nil);
//Scan all characters before next #
}
println(substrings.description);
//Now you got your substrings in an array, so use those for your data passing (in a segue maybe?)
...
}
}
}
extension UITextView {
class func getWordAtPosition(position: CGPoint!, textView: UITextView!) -> String? {
//Remove scrolloffset
let correctedPoint = CGPointMake(position.x, textView.contentOffset.y + position.y);
//Get location in text from uitextposition at a certian point
let tapPosition = textView.closestPositionToPoint(correctedPoint);
//Get word at the position, will return nil if its empty.
let wordRange = textView.tokenizer.rangeEnclosingPosition(tapPosition, withGranularity: UITextGranularity.Word, inDirection: UITextLayoutDirection.Right.rawValue);
return textView.textInRange(wordRange!);
}
}
Objective-C
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
UITapGestureRecognizer *gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(textViewTapped:)];
gestureRecognizer.numberOfTouchesRequired = 1;
gestureRecognizer.numberOfTapsRequired = 1;
[self.textView addGestureRecognizer:gestureRecognizer];
}
- (void)textViewTapped:(UITapGestureRecognizer *)sender {
NSString *wordTarget = #"here";
NSString* word = [self getWordAtPosition:[sender locationInView:self.textView] textView:self.textView];
if ([word isEqualToString:wordTarget]) {
NSString *plainString = self.textView.attributedText.string;
NSMutableArray* substrings = [[NSMutableArray alloc]init];
NSScanner *scanner = [[NSScanner alloc]initWithString:plainString];
[scanner scanUpToString:#"#" intoString:nil];
while (![scanner isAtEnd]) {
NSString* substring = nil;
[scanner scanString:#"#" intoString:nil];
NSString* space = #" ";
if ([scanner scanUpToString:space intoString:&substring]) {
[substrings addObject:substring];
}
[scanner scanUpToString:#"#" intoString:nil];
}
//Now you got your substrings in an array, so use those for your data passing (in a segue maybe?)
...
}
}
- (NSString*)getWordAtPosition:(CGPoint)position textView:(UITextView *)textView {
//remove scrollOffset
CGPoint correctedPoint = CGPointMake(position.x, textView.contentOffset.y + position.y);
UITextPosition *tapPosition = [textView closestPositionToPoint:correctedPoint];
UITextRange *wordRange = [textView.tokenizer rangeEnclosingPosition:tapPosition withGranularity:UITextGranularityWord inDirection:UITextLayoutDirectionRight];
return [textView textInRange:wordRange];
}
Basically you need to add a gesture recognizer to get the tap point in your textview. Then, you get the word using the category method provided in the extension area. After, you check what the word is (where we want the word "here"). Then, we collect the hashtags you have provided.
All you have to do is add a performSegueWithIdentifier method, and pass it accordingly.
In addition to #Nate Lee answer, updated the extension for Swift 4.0 version:
extension UITextView {
class func getWordAtPosition(position: CGPoint!, textView: UITextView!) -> String? {
//Remove scrolloffset
let correctedPoint = CGPoint(x: position.x, y: (textView.contentOffset.y + position.y))
//Get location in text from uitextposition at a certian point
let tapPosition = textView.closestPosition(to: correctedPoint)
//Get word at the position, will return nil if its empty.
let wordRange = textView.tokenizer.rangeEnclosingPosition(tapPosition!, with: .word, inDirection: UITextLayoutDirection.right.rawValue)
return textView.text(in: wordRange!)
}
}
Swift 3:
Don't check on the URL.scheme attribute. Returned nil for me.
Do this:
attributedString.addAttribute(NSLinkAttributeName, value: "openToViewController", range: range)
Then use the absoluteString attribute on the URL to check on that value to your view of choice:
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool{
if (URL.absoluteString == "openToViewController") {
let viewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "ViewController") as! UIViewController
self.present(viewController, animated: true, completion: nil)
return false
}
return true
}
Related
I have a text view with a string:
#"The best football player in the world is #OPTION, and the best basketball player is #OPTION?"
This is an example what I have to do. I want to replace #OPTION with a dropdown list, which means with custom view. It depends on the question, it can be only one #OPTION or more. Thanks in advance.
Edited:
The string is from API and it appears in UITextView or UILabel
After understanding your needs, The main issue you are facing is not knowing where to add the select lists.
I have created 2 categories for your case, for UILabel and for UITextView,
following these posts which contain the relevant answers for that:
How do I locate the CGRect for a substring of text in a UILabel?
Get X and Y coordinates of a word in UITextView
These categories find the CGRect for a string inside, which is where you should position your pickers.
The down-part of this for UILabel, is that it doesn't handle wordWrap line breaking mode well, and therefore it won't find the correct location, to handle this correctly, you should add line breaks when needed in case you use the UILabel.
UILabel:
extension UILabel
{
func rectFor(string str : String, fromIndex: Int = 0) -> (CGRect, NSRange)?
{
// Find the range of the string
guard self.text != nil else { return nil }
let subStringToSearch : NSString = (self.text! as NSString).substring(from: fromIndex) as NSString
var stringRange = subStringToSearch.range(of: str)
if (stringRange.location != NSNotFound)
{
guard self.attributedText != nil else { return nil }
// Add the starting point to the sub string
stringRange.location += fromIndex
let storage = NSTextStorage(attributedString: self.attributedText!)
let layoutManager = NSLayoutManager()
storage.addLayoutManager(layoutManager)
let textContainer = NSTextContainer(size: self.frame.size)
textContainer.lineFragmentPadding = 0
textContainer.lineBreakMode = .byWordWrapping
layoutManager.addTextContainer(textContainer)
var glyphRange = NSRange()
layoutManager.characterRange(forGlyphRange: stringRange, actualGlyphRange: &glyphRange)
let resultRect = layoutManager.boundingRect(forGlyphRange: glyphRange, in:textContainer)
return (resultRect, stringRange)
}
return nil
}
}
Usage for infinite searching all available substring (I recommend adding it in viewDidLayoutSubviews() in case you use auto-layout:
var lastFoundIndex : Int = 0
while let result = self.label.rectFor(string: "#OPTION", fromIndex: lastFoundIndex)
{
let view : UIView = UIView(frame: result.0)
view.backgroundColor = UIColor.red
self.label.addSubview(view)
lastFoundIndex = result.1.location + 1
}
And the same one for UITextView:
extension UITextView
{
func rectFor(string str : String, fromIndex: Int = 0) -> (CGRect, NSRange)?
{
// Find the range of the string
guard self.text != nil else { return nil }
let subStringToSearch : NSString = (self.text! as NSString).substring(from: fromIndex) as NSString
var stringRange = subStringToSearch.range(of: str)
if (stringRange.location != NSNotFound)
{
guard self.attributedText != nil else { return nil }
// Add the starting point to the sub string
stringRange.location += fromIndex
// Find first position
let startPosition = self.position(from: self.beginningOfDocument, offset: stringRange.location)
let endPosition = self.position(from: startPosition!, offset: stringRange.length)
let resultRange = self.textRange(from: startPosition!, to: endPosition!)
let resultRect = self.firstRect(for: resultRange!)
return (resultRect, stringRange)
}
return nil
}
}
Usage:
var lastFoundTextIndex : Int = 0
while let result = self.textView.rectFor(string: "#OPTION", fromIndex: lastFoundTextIndex)
{
let view : UIView = UIView(frame: result.0)
view.backgroundColor = UIColor.red
self.textView.addSubview(view)
lastFoundTextIndex = result.1.location + 1
}
In your case, textview gives the best results and uses included methods for that, the sample code uses a label & a text view, initialized in the code:
self.label.text = "The best football player in the world is\n#OPTION, and the best basketball player\n is #OPTION?"
self.textView.text = "The best football player in the world is #OPTION, and the best basketball player is #OPTION?"
And the output just adds views on top of the "#OPTION" strings:
Hope this helps
EDIT - Added Objective-C Variation:
Create 2 extensions - 1 for UITextView and 1 for UILabel:
UILabel:
#interface UILabel (UILabel_SubStringRect)
- (NSDictionary*)rectForString:(NSString*)string fromIndex:(int)index;
#end
#import "UILabel+SubStringRect.h"
#implementation UILabel (UILabel_SubStringRect)
- (NSDictionary*)rectForString:(NSString*)string fromIndex:(int)index
{
if (string != nil)
{
NSString* subStringToSearch = [self.text substringFromIndex:index];
NSRange stringRange = [subStringToSearch rangeOfString:string];
if (stringRange.location != NSNotFound)
{
if (self.attributedText != nil)
{
stringRange.location += index;
NSTextStorage* storage = [[NSTextStorage alloc] initWithAttributedString:self.attributedText];
NSLayoutManager* layoutManager = [NSLayoutManager new];
[storage addLayoutManager:layoutManager];
NSTextContainer* textContainer = [[NSTextContainer alloc] initWithSize:self.frame.size];
textContainer.lineFragmentPadding = 0;
textContainer.lineBreakMode = NSLineBreakByWordWrapping;
[layoutManager addTextContainer:textContainer];
NSRange glyphRange;
[layoutManager characterRangeForGlyphRange:stringRange actualGlyphRange:&glyphRange];
CGRect resultRect = [layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer];
return #{ #"rect" : [NSValue valueWithCGRect:resultRect], #"range" : [NSValue valueWithRange:stringRange] };
}
}
}
return nil;
}
#end
UITextView:
#interface UITextView (SubStringRect)
- (NSDictionary*)rectForString:(NSString*)string fromIndex:(int)index;
#end
#import "UITextView+SubStringRect.h"
#implementation UITextView (SubStringRect)
- (NSDictionary*)rectForString:(NSString*)string fromIndex:(int)index
{
if (string != nil)
{
NSString* subStringToSearch = [self.text substringFromIndex:index];
NSRange stringRange = [subStringToSearch rangeOfString:string];
if (stringRange.location != NSNotFound)
{
if (self.attributedText != nil)
{
stringRange.location += index;
UITextPosition* startPosition = [self positionFromPosition:self.beginningOfDocument offset:stringRange.location];
UITextPosition* endPosition = [self positionFromPosition:startPosition offset:stringRange.length];
UITextRange* resultRange = [self textRangeFromPosition:startPosition toPosition:endPosition];
CGRect resultRect = [self firstRectForRange:resultRange];
return #{ #"rect" : [NSValue valueWithCGRect:resultRect], #"range" : [NSValue valueWithRange:stringRange] };
}
}
}
return nil;
}
#end
Usage Sample - UILabel:
int lastFoundIndex = 0;
NSDictionary* resultDict = nil;
do
{
resultDict = [self.label rectForString:#"#OPTION" fromIndex:lastFoundIndex];
if (resultDict != nil)
{
NSLog(#"result: %#", resultDict[#"rect"]);
UIView* view = [[UIView alloc] initWithFrame:[resultDict[#"rect"] CGRectValue]];
[view setBackgroundColor:[UIColor redColor]];
[self.label addSubview:view];
lastFoundIndex = (int)[resultDict[#"range"] rangeValue].location + 1;
}
} while (resultDict != nil);
UITextView:
int lastFoundTextIndex = 0;
NSDictionary* resultTextDict = nil;
do
{
resultTextDict = [self.textview rectForString:#"#OPTION" fromIndex:lastFoundTextIndex];
if (resultTextDict != nil)
{
NSLog(#"result: %#", resultTextDict[#"rect"]);
UIView* view = [[UIView alloc] initWithFrame:[resultTextDict[#"rect"] CGRectValue]];
[view setBackgroundColor:[UIColor redColor]];
[self.textview addSubview:view];
lastFoundTextIndex = (int)[resultTextDict[#"range"] rangeValue].location + 1;
}
} while (resultTextDict != nil);
I have a UITextView which displays an NSAttributedString. This string contains words that I'd like to make tappable, such that when they are tapped I get called back so that I can perform an action. I realise that UITextView can detect taps on a URL and call back my delegate, but these aren't URLs.
It seems to me that with iOS 7 and the power of TextKit this should now be possible, however I can't find any examples and I'm not sure where to start.
I understand that it's now possible to create custom attributes in the string (although I haven't done this yet), and perhaps these will be useful to detecting if one of the magic words has been tapped? In any case, I still don't know how to intercept that tap and detect on which word the tap occurred.
Note that iOS 6 compatibility is not required.
I just wanted to help others a little more. Following on from Shmidt's response it's possible to do exactly as I had asked in my original question.
1) Create an attributed string with custom attributes applied to the clickable words. eg.
NSAttributedString* attributedString = [[NSAttributedString alloc] initWithString:#"a clickable word" attributes:#{ #"myCustomTag" : #(YES) }];
[paragraph appendAttributedString:attributedString];
2) Create a UITextView to display that string, and add a UITapGestureRecognizer to it. Then handle the tap:
- (void)textTapped:(UITapGestureRecognizer *)recognizer
{
UITextView *textView = (UITextView *)recognizer.view;
// Location of the tap in text-container coordinates
NSLayoutManager *layoutManager = textView.layoutManager;
CGPoint location = [recognizer locationInView:textView];
location.x -= textView.textContainerInset.left;
location.y -= textView.textContainerInset.top;
// Find the character that's been tapped on
NSUInteger characterIndex;
characterIndex = [layoutManager characterIndexForPoint:location
inTextContainer:textView.textContainer
fractionOfDistanceBetweenInsertionPoints:NULL];
if (characterIndex < textView.textStorage.length) {
NSRange range;
id value = [textView.attributedText attribute:#"myCustomTag" atIndex:characterIndex effectiveRange:&range];
// Handle as required...
NSLog(#"%#, %d, %d", value, range.location, range.length);
}
}
So easy when you know how!
Detecting taps on attributed text with Swift
Sometimes for beginners it is a little hard to know how to do get things set up (it was for me anyway), so this example is a little fuller.
Add a UITextView to your project.
Outlet
Connect the UITextView to the ViewController with an outlet named textView.
Custom attribute
We are going to make a custom attribute by making an Extension.
Note: This step is technically optional, but if you don't do it you will need to edit the code in the next part to use a standard attribute like NSAttributedString.Key.foregroundColor. The advantage of using a custom attribute is that you can define what values you want to store in the attributed text range.
Add a new swift file with File > New > File... > iOS > Source > Swift File. You can call it what you want. I am calling mine NSAttributedStringKey+CustomAttribute.swift.
Paste in the following code:
import Foundation
extension NSAttributedString.Key {
static let myAttributeName = NSAttributedString.Key(rawValue: "MyCustomAttribute")
}
Code
Replace the code in ViewController.swift with the following. Note the UIGestureRecognizerDelegate.
import UIKit
class ViewController: UIViewController, UIGestureRecognizerDelegate {
#IBOutlet weak var textView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
// Create an attributed string
let myString = NSMutableAttributedString(string: "Swift attributed text")
// Set an attribute on part of the string
let myRange = NSRange(location: 0, length: 5) // range of "Swift"
let myCustomAttribute = [ NSAttributedString.Key.myAttributeName: "some value"]
myString.addAttributes(myCustomAttribute, range: myRange)
textView.attributedText = myString
// Add tap gesture recognizer to Text View
let tap = UITapGestureRecognizer(target: self, action: #selector(myMethodToHandleTap(_:)))
tap.delegate = self
textView.addGestureRecognizer(tap)
}
#objc func myMethodToHandleTap(_ sender: UITapGestureRecognizer) {
let myTextView = sender.view as! UITextView
let layoutManager = myTextView.layoutManager
// location of tap in myTextView coordinates and taking the inset into account
var location = sender.location(in: myTextView)
location.x -= myTextView.textContainerInset.left;
location.y -= myTextView.textContainerInset.top;
// character index at tap location
let characterIndex = layoutManager.characterIndex(for: location, in: myTextView.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
// if index is valid then do something.
if characterIndex < myTextView.textStorage.length {
// print the character index
print("character index: \(characterIndex)")
// print the character at the index
let myRange = NSRange(location: characterIndex, length: 1)
let substring = (myTextView.attributedText.string as NSString).substring(with: myRange)
print("character at index: \(substring)")
// check if the tap location has a certain attribute
let attributeName = NSAttributedString.Key.myAttributeName
let attributeValue = myTextView.attributedText?.attribute(attributeName, at: characterIndex, effectiveRange: nil)
if let value = attributeValue {
print("You tapped on \(attributeName.rawValue) and the value is: \(value)")
}
}
}
}
Now if you tap on the "w" of "Swift", you should get the following result:
character index: 1
character at index: w
You tapped on MyCustomAttribute and the value is: some value
Notes
Here I used a custom attribute, but it could have just as easily been NSAttributedString.Key.foregroundColor (text color) that has a value of UIColor.green.
Formerly the text view could not be editable or selectable, but in my updated answer for Swift 4.2 it seems to be working fine no matter whether these are selected or not.
Further study
This answer was based on several other answers to this question. Besides these, see also
Advanced Text Layouts and Effects with Text Kit (WWDC 2013 video)
Attributed String Programming Guide
How do I make an attributed string using Swift?
This is a slightly modified version, building off of #tarmes answer. I couldn't get the valuevariable to return anything but null without the tweak below. Also, I needed the full attribute dictionary returned in order to determine the resulting action. I would have put this in the comments but don't appear to have the rep to do so. Apologies in advance if I have violated protocol.
Specific tweak is to use textView.textStorage instead of textView.attributedText. As a still learning iOS programmer, I am not really sure why this is, but perhaps someone else can enlighten us.
Specific modification in the tap handling method:
NSDictionary *attributesOfTappedText = [textView.textStorage attributesAtIndex:characterIndex effectiveRange:&range];
Full code in my view controller
- (void)viewDidLoad
{
[super viewDidLoad];
self.textView.attributedText = [self attributedTextViewString];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(textTapped:)];
[self.textView addGestureRecognizer:tap];
}
- (NSAttributedString *)attributedTextViewString
{
NSMutableAttributedString *paragraph = [[NSMutableAttributedString alloc] initWithString:#"This is a string with " attributes:#{NSForegroundColorAttributeName:[UIColor blueColor]}];
NSAttributedString* attributedString = [[NSAttributedString alloc] initWithString:#"a tappable string"
attributes:#{#"tappable":#(YES),
#"networkCallRequired": #(YES),
#"loadCatPicture": #(NO)}];
NSAttributedString* anotherAttributedString = [[NSAttributedString alloc] initWithString:#" and another tappable string"
attributes:#{#"tappable":#(YES),
#"networkCallRequired": #(NO),
#"loadCatPicture": #(YES)}];
[paragraph appendAttributedString:attributedString];
[paragraph appendAttributedString:anotherAttributedString];
return [paragraph copy];
}
- (void)textTapped:(UITapGestureRecognizer *)recognizer
{
UITextView *textView = (UITextView *)recognizer.view;
// Location of the tap in text-container coordinates
NSLayoutManager *layoutManager = textView.layoutManager;
CGPoint location = [recognizer locationInView:textView];
location.x -= textView.textContainerInset.left;
location.y -= textView.textContainerInset.top;
NSLog(#"location: %#", NSStringFromCGPoint(location));
// Find the character that's been tapped on
NSUInteger characterIndex;
characterIndex = [layoutManager characterIndexForPoint:location
inTextContainer:textView.textContainer
fractionOfDistanceBetweenInsertionPoints:NULL];
if (characterIndex < textView.textStorage.length) {
NSRange range;
NSDictionary *attributes = [textView.textStorage attributesAtIndex:characterIndex effectiveRange:&range];
NSLog(#"%#, %#", attributes, NSStringFromRange(range));
//Based on the attributes, do something
///if ([attributes objectForKey:...)] //make a network call, load a cat Pic, etc
}
}
Making custom link and doing what you want on the tap has become much easier with iOS 7.
There is very good example at Ray Wenderlich
WWDC 2013 example:
NSLayoutManager *layoutManager = textView.layoutManager;
CGPoint location = [touch locationInView:textView];
NSUInteger characterIndex;
characterIndex = [layoutManager characterIndexForPoint:location
inTextContainer:textView.textContainer
fractionOfDistanceBetweenInsertionPoints:NULL];
if (characterIndex < textView.textStorage.length) {
// valid index
// Find the word range here
// using -enumerateSubstringsInRange:options:usingBlock:
}
I was able to solve this pretty simply with NSLinkAttributeName
Swift 2
class MyClass: UIViewController, UITextViewDelegate {
#IBOutlet weak var tvBottom: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
let attributedString = NSMutableAttributedString(string: "click me ok?")
attributedString.addAttribute(NSLinkAttributeName, value: "cs://moreinfo", range: NSMakeRange(0, 5))
tvBottom.attributedText = attributedString
tvBottom.delegate = self
}
func textView(textView: UITextView, shouldInteractWithURL URL: NSURL, inRange characterRange: NSRange) -> Bool {
UtilityFunctions.alert("clicked", message: "clicked")
return false
}
}
Complete example for detect actions on attributed text with Swift 3
let termsAndConditionsURL = TERMS_CONDITIONS_URL;
let privacyURL = PRIVACY_URL;
override func viewDidLoad() {
super.viewDidLoad()
self.txtView.delegate = self
let str = "By continuing, you accept the Terms of use and Privacy policy"
let attributedString = NSMutableAttributedString(string: str)
var foundRange = attributedString.mutableString.range(of: "Terms of use") //mention the parts of the attributed text you want to tap and get an custom action
attributedString.addAttribute(NSLinkAttributeName, value: termsAndConditionsURL, range: foundRange)
foundRange = attributedString.mutableString.range(of: "Privacy policy")
attributedString.addAttribute(NSLinkAttributeName, value: privacyURL, range: foundRange)
txtView.attributedText = attributedString
}
And then you can catch the action with shouldInteractWith URL UITextViewDelegate delegate method.So make sure you have set the delegate properly.
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "WebView") as! SKWebViewController
if (URL.absoluteString == termsAndConditionsURL) {
vc.strWebURL = TERMS_CONDITIONS_URL
self.navigationController?.pushViewController(vc, animated: true)
} else if (URL.absoluteString == privacyURL) {
vc.strWebURL = PRIVACY_URL
self.navigationController?.pushViewController(vc, animated: true)
}
return false
}
Like wise you can perform any action according to your requirement.
Cheers!!
It's possible to do that with characterIndexForPoint:inTextContainer:fractionOfDistanceBetweenInsertionPoints:. It'll work somewhat differently than you wanted - you'll have to test if a tapped character belongs to a magic word. But it shouldn't be complicated.
BTW I highly recommend watching Introducing Text Kit from WWDC 2013.
With Swift 5 and iOS 12, you can create a subclass of UITextView and override point(inside:with:) with some TextKit implementation in order to make only some NSAttributedStrings in it tappable.
The following code shows how to create a UITextView that only reacts to taps on underlined NSAttributedStrings in it:
InteractiveUnderlinedTextView.swift
import UIKit
class InteractiveUnderlinedTextView: UITextView {
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
func configure() {
isScrollEnabled = false
isEditable = false
isSelectable = false
isUserInteractionEnabled = true
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let superBool = super.point(inside: point, with: event)
let characterIndex = layoutManager.characterIndex(for: point, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
guard characterIndex < textStorage.length else { return false }
let attributes = textStorage.attributes(at: characterIndex, effectiveRange: nil)
return superBool && attributes[NSAttributedString.Key.underlineStyle] != nil
}
}
ViewController.swift
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let linkTextView = InteractiveUnderlinedTextView()
linkTextView.backgroundColor = .orange
let mutableAttributedString = NSMutableAttributedString(string: "Some text\n\n")
let attributes = [NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue]
let underlinedAttributedString = NSAttributedString(string: "Some other text", attributes: attributes)
mutableAttributedString.append(underlinedAttributedString)
linkTextView.attributedText = mutableAttributedString
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(underlinedTextTapped))
linkTextView.addGestureRecognizer(tapGesture)
view.addSubview(linkTextView)
linkTextView.translatesAutoresizingMaskIntoConstraints = false
linkTextView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
linkTextView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
linkTextView.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor).isActive = true
}
#objc func underlinedTextTapped(_ sender: UITapGestureRecognizer) {
print("Hello")
}
}
Use this extension for Swift:
import UIKit
extension UITapGestureRecognizer {
func didTapAttributedTextInTextView(textView: UITextView, inRange targetRange: NSRange) -> Bool {
let layoutManager = textView.layoutManager
let locationOfTouch = self.location(in: textView)
let index = layoutManager.characterIndex(for: locationOfTouch, in: textView.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
return NSLocationInRange(index, targetRange)
}
}
Add UITapGestureRecognizer to your text view with following selector:
guard let text = textView.attributedText?.string else {
return
}
let textToTap = "Tap me"
if let range = text.range(of: textToTap),
tapGesture.didTapAttributedTextInTextView(textView: textTextView, inRange: NSRange(range, in: text)) {
// Tap recognized
}
This one might work OK with short link, multilink in a textview. It work OK with iOS 6,7,8.
- (void)tappedTextView:(UITapGestureRecognizer *)tapGesture {
if (tapGesture.state != UIGestureRecognizerStateEnded) {
return;
}
UITextView *textView = (UITextView *)tapGesture.view;
CGPoint tapLocation = [tapGesture locationInView:textView];
NSDataDetector *detector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink|NSTextCheckingTypePhoneNumber
error:nil];
NSArray* resultString = [detector matchesInString:self.txtMessage.text options:NSMatchingReportProgress range:NSMakeRange(0, [self.txtMessage.text length])];
BOOL isContainLink = resultString.count > 0;
if (isContainLink) {
for (NSTextCheckingResult* result in resultString) {
CGRect linkPosition = [self frameOfTextRange:result.range inTextView:self.txtMessage];
if(CGRectContainsPoint(linkPosition, tapLocation) == 1){
if (result.resultType == NSTextCheckingTypePhoneNumber) {
NSString *phoneNumber = [#"telprompt://" stringByAppendingString:result.phoneNumber];
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:phoneNumber]];
}
else if (result.resultType == NSTextCheckingTypeLink) {
[[UIApplication sharedApplication] openURL:result.URL];
}
}
}
}
}
- (CGRect)frameOfTextRange:(NSRange)range inTextView:(UITextView *)textView
{
UITextPosition *beginning = textView.beginningOfDocument;
UITextPosition *start = [textView positionFromPosition:beginning offset:range.location];
UITextPosition *end = [textView positionFromPosition:start offset:range.length];
UITextRange *textRange = [textView textRangeFromPosition:start toPosition:end];
CGRect firstRect = [textView firstRectForRange:textRange];
CGRect newRect = [textView convertRect:firstRect fromView:textView.textInputView];
return newRect;
}
This has changed as of iOS 10. In iOS 10, you can use the .link attribute and it all just works.
No need for custom attributes, tap gesture recognisers or anything. It works like an ordinary URL.
To do this, instead of adding the url to the NSMutableAttributedString, add what you want to call the url instead (eg, 'cats' to go to the wikipedia page about cats), and then add the standard attribute NSAttributedString.Key.link (I'm using Swift here), with the NSURL containing the target URL.
Reference: https://medium.com/real-solutions-artificial-intelligence/create-clickable-links-with-nsmutableattributedstring-12b6661a357d
I been trying to use AsyncDisplayKit framework , I have an requirement to detect url's in text.
I have used ASTextNode but couldn't find any api to detect links.
I read that there is property linkAttributeNames used for url detection but unable to find any example how to do it.
Could someone help me how to use the above class?
thanks
For Swift 3.0
func addLinkDetection(_ text: String, highLightColor: UIColor, delegate: ASTextNodeDelegate) {
self.isUserInteractionEnabled = true
self.delegate = delegate
let types: NSTextCheckingResult.CheckingType = [.link]
let detector = try? NSDataDetector(types: types.rawValue)
let range = NSMakeRange(0, text.characters.count)
if let attributedText = self.attributedText {
let mutableString = NSMutableAttributedString()
mutableString.append(attributedText)
detector?.enumerateMatches(in: text, range: range) {
(result, _, _) in
if let fixedRange = result?.range {
mutableString.addAttribute(NSUnderlineColorAttributeName, value: highLightColor, range: fixedRange)
mutableString.addAttribute(NSLinkAttributeName, value: result?.url, range: fixedRange)
mutableString.addAttribute(NSForegroundColorAttributeName, value: highLightColor, range: fixedRange)
}
}
self.attributedText = mutableString
}
}
Add the delegate to your viewController:
/// Delegate function for linkDetection
func textNode(_ textNode: ASTextNode, shouldHighlightLinkAttribute attribute: String, value: Any, at point: CGPoint) -> Bool {
return true
}
func textNode(_ textNode: ASTextNode, tappedLinkAttribute attribute: String, value: Any, at point: CGPoint, textRange: NSRange) {
guard let url = value as? URL else { return }
}
For link detection you need to use external library.
I'd recommend https://github.com/twitter/twitter-text
You can install it with cocoapods.
Then you need to convert TwitterTextEntity* to NSTextCheckingResult*.
You can use this category of NSString:
- (NSArray <NSTextCheckingResult *>*)textCheckingResultsForURLs {
NSArray *twitterEntitiesArray = [TwitterText URLsInText:self];
NSMutableArray *textCheckingResultsArray = [[NSMutableArray alloc] initWithCapacity:[twitterEntitiesArray count]];
for (TwitterTextEntity *twitterTextEntity in twitterEntitiesArray) {
NSString *textCheckingResultUTF8 = [[self substringWithRange:twitterTextEntity.range] stringPercentEncode];
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:#"%#", textCheckingResultUTF8]];
NSTextCheckingResult *result = [NSTextCheckingResult linkCheckingResultWithRange:twitterTextEntity.range URL:url];
[textCheckingResultsArray addObject:result];
}
return textCheckingResultsArray;
}
Use it like this:
NSArray *links = [yourString textCheckingResultsForURLs];
Then you need to add calculated ranges to NSMutableAttributedString like this:
for (NSTextCheckingResult *textCheckingResult in links) {
NSMutableDictionary *linkAttributes = [[NSMutableDictionary alloc] initWithDictionary:#{NSForegroundColorAttributeName : [UIColor whiteColor]}];
linkAttributes[#"TextLinkAttributeNameURL"] = [NSURL URLWithString:textCheckingResult.URL.absoluteString];
[string addAttributes:linkAttributes range:textCheckingResult.range];
}
Then you need to configure ASTextNode node to highlight specific ranges. So in parent node add:
_textLabelNode.delegate = self;
_textLabelNode.userInteractionEnabled = YES;
_textLabelNode.linkAttributeNames = #[#"TextLinkAttributeNameURL"];
+
- (void)didLoad {
// For text node
self.layer.as_allowsHighlightDrawing = YES;
[super didLoad];
}
#pragma mark - ASTextNodeDelegate
- (BOOL)textNode:(ASTextNode *)richTextNode shouldHighlightLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point {
return YES;
}
- (void)textNode:(ASTextNode *)richTextNode tappedLinkAttribute:(NSString *)attribute value:(NSURL *)URL atPoint:(CGPoint)point textRange:(NSRange)textRange {
NSLog(#"TODO");
}
This works for me. Hope, didn't forget about anything.
I am working on new iOS application. In that app, I have 5 UITextFields and those are
1. first interest, second interest upto 5 interests.
I need to add Autocomplete for those 5 UITextFields. I have searched google for one day. I got some forums and tutorial for that. But I even have tried with Github links also.
According to my requirement, I have an array of data which is getting from my server. In that array, I have data like, coffee, cricket, etc. That is Autocomplete data. I need to display that array whenever user entered text in UITextField, if its related to my array of data, need to display below of that UITextFields.
For that purpose i used following code.
**// String in Search textfield
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
NSString *substring = [NSString stringWithString:textField.text];
substring = [substring stringByReplacingCharactersInRange:range withString:string];
[self searchAutocompleteEntriesWithSubstring:substring];
return YES;
}
**
// Take string from Search Textfield and compare it with autocomplete array
- (void)searchAutocompleteEntriesWithSubstring:(NSString *)substring {
// Put anything that starts with this substring into the autoCompleteArray
// The items in this array is what will show up in the table view
[autoCompleteArray removeAllObjects];
NSLog(#"autoCompleteArray %#",autoCompleteArray);
for(NSString *curString in elementArray) {
NSRange substringRangeLowerCase = [curString rangeOfString:[substring lowercaseString]];
NSRange substringRangeUpperCase = [curString rangeOfString:[substring uppercaseString]];
if (substringRangeLowerCase.length != 0 || substringRangeUpperCase.length != 0) {
[autoCompleteArray addObject:curString];
}
}
autoCompleteTableView.hidden = NO;
[autoCompleteTableView reloadData];
}
And I created UITableView as AutocompleteTableview in ViewDidLoad method.
**Issue is, if I type text as suppose "c", from my array of data displaying whatever text contains as "c" letter in tableview. But, if i typed "coff" no data displaying in that UITableView. Also how to validate which UITextField user clicking in tableviewdidselectrowatindexpath delegate method. I tried with assigning tag for those UITextFields, but its working in UITextFields delegate methods only, not in other place. so, whenever i selected data from UITableView, first UITextField only taking data not other UITextField.
Please give your valuable suggestion, which is the best way to display the autocomplete for UITextfields in iOS for multiple UITextfields and how to handle UITableView for displaying data. If anything mistakes in my content forgive me and please provide your valuable suggestions to fix this issue.
Thanks****
Here is swift 3 inline autocomplete textfield example
Image look like bellow
create a project & add a textfield. Connect to viewcontroller named txtAutoComplete
View controller code bellow
import UIKit
class ViewController: UIViewController ,UITextFieldDelegate{
#IBOutlet weak var txtAutoComplete: UITextField!
var autoCompletionPossibilities = ["01921687433", "01553377642", "0155776622"]
var autoCompleteCharacterCount = 0
var timer = Timer()
override func viewDidLoad() {
super.viewDidLoad()
txtAutoComplete.delegate = self
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { //1
var subString = (textField.text!.capitalized as NSString).replacingCharacters(in: range, with: string)
subString = formatSubstring(subString: subString)
if subString.characters.count == 0 {
// 3 when a user clears the textField
resetValues()
} else {
searchAutocompleteEntriesWIthSubstring(substring: subString)
}
return true
}
func formatSubstring(subString: String) -> String {
let formatted = String(subString.characters.dropLast(autoCompleteCharacterCount)).lowercased().capitalized //5
return formatted
}
func resetValues() {
autoCompleteCharacterCount = 0
txtAutoComplete.text = ""
}
func searchAutocompleteEntriesWIthSubstring(substring: String) {
let userQuery = substring
let suggestions = getAutocompleteSuggestions(userText: substring)
if suggestions.count > 0 {
timer = .scheduledTimer(withTimeInterval: 0.01, repeats: false, block: { (timer) in //2
let autocompleteResult = self.formatAutocompleteResult(substring: substring, possibleMatches: suggestions)
self.putColourFormattedTextInTextField(autocompleteResult: autocompleteResult, userQuery : userQuery)
self.moveCaretToEndOfUserQueryPosition(userQuery: userQuery)
})
} else {
timer = .scheduledTimer(withTimeInterval: 0.01, repeats: false, block: { (timer) in //7
self.txtAutoComplete.text = substring
})
autoCompleteCharacterCount = 0
}
}
func getAutocompleteSuggestions(userText: String) -> [String]{
var possibleMatches: [String] = []
for item in autoCompletionPossibilities { //2
let myString:NSString! = item as NSString
let substringRange :NSRange! = myString.range(of: userText)
if (substringRange.location == 0)
{
possibleMatches.append(item)
}
}
return possibleMatches
}
func putColourFormattedTextInTextField(autocompleteResult: String, userQuery : String) {
let colouredString: NSMutableAttributedString = NSMutableAttributedString(string: userQuery + autocompleteResult)
colouredString.addAttribute(NSForegroundColorAttributeName, value: UIColor.green, range: NSRange(location: userQuery.characters.count,length:autocompleteResult.characters.count))
self.txtAutoComplete.attributedText = colouredString
}
func moveCaretToEndOfUserQueryPosition(userQuery : String) {
if let newPosition = self.txtAutoComplete.position(from: self.txtAutoComplete.beginningOfDocument, offset: userQuery.characters.count) {
self.txtAutoComplete.selectedTextRange = self.txtAutoComplete.textRange(from: newPosition, to: newPosition)
}
let selectedRange: UITextRange? = txtAutoComplete.selectedTextRange
txtAutoComplete.offset(from: txtAutoComplete.beginningOfDocument, to: (selectedRange?.start)!)
}
func formatAutocompleteResult(substring: String, possibleMatches: [String]) -> String {
var autoCompleteResult = possibleMatches[0]
autoCompleteResult.removeSubrange(autoCompleteResult.startIndex..<autoCompleteResult.index(autoCompleteResult.startIndex, offsetBy: substring.characters.count))
autoCompleteCharacterCount = autoCompleteResult.characters.count
return autoCompleteResult
}
}
Source code is given to GitHub.GitHub Link :https://github.com/enamul95/AutoCompleteTextField
Take two Global Array
NSMutableArray *muary_Interest_Main;
NSMutableArray *muary_Interest_Sub;
IN viewDidLoad Method
muary_Interest_Main = [[NSMutableArray alloc]initWithObjects:#"Cricket",#"Dancing",#"Painting",#"Swiming",#"guitar",#"movie",#"boxing",#"drum",#"hockey",#"chessing",#"gamming",
#"hunting",#"killing",#"shoping",#"jamm"#"zooming", nil];
muary_Interest_Sub = [[NSMutableArray alloc]init];
tbl_Search = [[UITableView alloc] initWithFrame:
CGRectMake(4, 200, 320, 120) style:UITableViewStylePlain];
tbl_Search.delegate = self;
tbl_Search.dataSource = self;
tbl_Search.scrollEnabled = YES;
[self.tbl_Search registerClass:[UITableViewCell class] forCellReuseIdentifier:#"CellIdentifier"];
[self.view addSubview:self.tbl_Search];
[tbl_Search setHidden:TRUE];
Now write a below code in textfield delegates.
- (void)textFieldDidBeginEditing:(UITextField *)textField
{
NSLog(#"%d",textField.tag);
int_TextFieldTag = textField.tag;
[self searchText:textField replacementString:#"Begin"];
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
[textField resignFirstResponder];
tbl_Search.hidden = TRUE;
return YES;
}
- (void)textFieldDidEndEditing:(UITextField *)textField
{
tbl_Search.hidden = TRUE;
}
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
[self searchText:textField replacementString:string];
return YES;
}
Write a method for search text
-(void) searchText:(UITextField *)textField replacementString:(NSString *)string
{
if(int_TextFieldTag == 1)
{
tbl_Search.frame = CGRectMake(4, 200, 320, 120);
}
else if(int_TextFieldTag == 2)
{
tbl_Search.frame = CGRectMake(4, 248, 320, 120);
}
else if(int_TextFieldTag == 3)
{
tbl_Search.frame = CGRectMake(4, 268, 320, 120);
}
else if(int_TextFieldTag == 4)
{
tbl_Search.frame = CGRectMake(4, 268, 320, 120);
}
else
{
tbl_Search.frame = CGRectMake(4, 268, 320, 120);
}
NSString *str_Search_String=[NSString stringWithFormat:#"%#",textField.text];
if([string isEqualToString:#"Begin"])
str_Search_String=[NSString stringWithFormat:#"%#",textField.text];
else if([string isEqualToString:#""])
str_Search_String = [str_Search_String substringToIndex:[str_Search_String length] - 1];
else
str_Search_String=[str_Search_String stringByAppendingString:string];
muary_Interest_Sub=[[NSMutableArray alloc] init];
if(str_Search_String.length>0)
{
NSInteger counter = 0;
for(NSString *name in muary_Interest_Main)
{
NSRange r = [name rangeOfString:str_Search_String options:NSCaseInsensitiveSearch];
if(r.length>0)
{
[muary_Interest_Sub addObject:name];
}
counter++;
}
if (muary_Interest_Sub.count > 0)
{
NSLog(#"%#",muary_Interest_Sub);
tbl_Search.hidden = FALSE;
[self.tbl_Search reloadData];
}
else
{
tbl_Search.hidden = TRUE;
}
}
else
{
[tbl_Search setHidden:TRUE];
}
}
Tableview Delegates methods
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [muary_Interest_Sub count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"CellIdentifier"];
if (cell == nil)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#"CellIdentifier"];
//cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
;
}
cell.textLabel.text = [muary_Interest_Sub objectAtIndex:indexPath.row];
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[self.view endEditing:YES];
if(int_TextFieldTag == 1)
{
txt1.text=[muary_Interest_Sub objectAtIndex:indexPath.row];
}
else if(int_TextFieldTag == 2)
{
txt2.text=[muary_Interest_Sub objectAtIndex:indexPath.row];
}
else if(int_TextFieldTag == 3)
{
txt3.text=[muary_Interest_Sub objectAtIndex:indexPath.row];
}
else if(int_TextFieldTag == 4)
{
txt4.text=[muary_Interest_Sub objectAtIndex:indexPath.row];
}
else
{
txt5.text=[muary_Interest_Sub objectAtIndex:indexPath.row];
}
}
This also works on backspace of textfield.
Try this. Hope this will suit your requirements.
- (void)searchAutocompleteEntriesWithSubstring:(NSString *)substring{
//Assume this array is the autocomplete array for which you get data from server
NSMutableArray *autoCompleteArray = [[NSMutableArray alloc] initWithObjects:#"Coffee",#"Cricket",#"Volleyboll",nil];
text = [text stringByReplacingOccurrencesOfString:#" " withString:#""];
// This is to create predicate filter for getting matched text
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"SELF beginswith[c] %#",text];
// store matched data for autocompletion in results array and reload data in your tableview based on this array's data
NSArray *resultArray = [[NSArray alloc] initWithArray:[autoCompleteArray filteredArrayUsingPredicate:predicate]];
}
You can use the following links which describes how we can use autocompletion. I tried with the AutocompletionTableview (second link) and it worked perfectly.
https://github.com/EddyBorja/MLPAutoCompleteTextField
https://github.com/keyf/AutocompletionTableView
I am implemententing a "read more" functionality much like the one in Apple's AppStore. However, I am using a multiline UILabel. Looking at Apple's AppStore, how do they decrease the last visible line's width to fit the "more" text and still truncate the tail (see image)?
This seems to work, at least with the limited amount of testing I've done. There are two public methods. You can use the shorter one if you have multiple labels all with the same number of lines -- just change the kNumberOfLines at the top to match what you want. Use the longer method if you need to pass the number of lines for different labels. Be sure to change the class of the labels you make in IB to RDLabel. Use these methods instead of setText:. These methods expand the height of the label to kNumberOfLines if necessary, and if still truncated, will expand it to fit the whole string on touch. Currently, you can touch anywhere in the label. It shouldn't be too hard to change that so only touches near the ...Mer would cause the expansion.
#import "RDLabel.h"
#define kNumberOfLines 2
#define ellipsis #"...Mer ▾ "
#implementation RDLabel {
NSString *string;
}
#pragma Public Methods
- (void)setTruncatingText:(NSString *) txt {
[self setTruncatingText:txt forNumberOfLines:kNumberOfLines];
}
- (void)setTruncatingText:(NSString *) txt forNumberOfLines:(int) lines{
string = txt;
self.numberOfLines = 0;
NSMutableString *truncatedString = [txt mutableCopy];
if ([self numberOfLinesNeeded:truncatedString] > lines) {
[truncatedString appendString:ellipsis];
NSRange range = NSMakeRange(truncatedString.length - (ellipsis.length + 1), 1);
while ([self numberOfLinesNeeded:truncatedString] > lines) {
[truncatedString deleteCharactersInRange:range];
range.location--;
}
[truncatedString deleteCharactersInRange:range]; //need to delete one more to make it fit
CGRect labelFrame = self.frame;
labelFrame.size.height = [#"A" sizeWithFont:self.font].height * lines;
self.frame = labelFrame;
self.text = truncatedString;
self.userInteractionEnabled = YES;
UITapGestureRecognizer *tapper = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(expand:)];
[self addGestureRecognizer:tapper];
}else{
CGRect labelFrame = self.frame;
labelFrame.size.height = [#"A" sizeWithFont:self.font].height * lines;
self.frame = labelFrame;
self.text = txt;
}
}
#pragma Private Methods
-(int)numberOfLinesNeeded:(NSString *) s {
float oneLineHeight = [#"A" sizeWithFont:self.font].height;
float totalHeight = [s sizeWithFont:self.font constrainedToSize:CGSizeMake(self.bounds.size.width, CGFLOAT_MAX) lineBreakMode:NSLineBreakByWordWrapping].height;
return nearbyint(totalHeight/oneLineHeight);
}
-(void)expand:(UITapGestureRecognizer *) tapper {
int linesNeeded = [self numberOfLinesNeeded:string];
CGRect labelFrame = self.frame;
labelFrame.size.height = [#"A" sizeWithFont:self.font].height * linesNeeded;
self.frame = labelFrame;
self.text = string;
}
Since this post is from 2013, I wanted to give my Swift implementation of the very nice solution from #rdelmar.
Considering we are using a SubClass of UILabel:
private let kNumberOfLines = 2
private let ellipsis = " MORE"
private var originalString: String! // Store the original text in the init
private func getTruncatingText() -> String {
var truncatedString = originalString.mutableCopy() as! String
if numberOfLinesNeeded(truncatedString) > kNumberOfLines {
truncatedString += ellipsis
var range = Range<String.Index>(
start: truncatedString.endIndex.advancedBy(-(ellipsis.characters.count + 1)),
end: truncatedString.endIndex.advancedBy(-ellipsis.characters.count)
)
while numberOfLinesNeeded(truncatedString) > kNumberOfLines {
truncatedString.removeRange(range)
range.startIndex = range.startIndex.advancedBy(-1)
range.endIndex = range.endIndex.advancedBy(-1)
}
}
return truncatedString
}
private func getHeightForString(str: String) -> CGFloat {
return str.boundingRectWithSize(
CGSizeMake(self.bounds.size.width, CGFloat.max),
options: [.UsesLineFragmentOrigin, .UsesFontLeading],
attributes: [NSFontAttributeName: font],
context: nil).height
}
private func numberOfLinesNeeded(s: String) -> Int {
let oneLineHeight = "A".sizeWithAttributes([NSFontAttributeName: font]).height
let totalHeight = getHeightForString(s)
return Int(totalHeight / oneLineHeight)
}
func expend() {
var labelFrame = self.frame
labelFrame.size.height = getHeightForString(originalString)
self.frame = labelFrame
self.text = originalString
}
func collapse() {
let truncatedText = getTruncatingText()
var labelFrame = self.frame
labelFrame.size.height = getHeightForString(truncatedText)
self.frame = labelFrame
self.text = truncatedText
}
Unlike the old solution, this will work as well for any kind of text attribute (like NSParagraphStyleAttributeName).
Please feel free to critic and comment. Thanks again to #rdelmar.
There are multiple ways to do this, with the most elegant being to use CoreText exclusively since you get complete control over how to display the text.
Here is a hybrid option where we use CoreText to recreate the label, determine where it ends, and then we cut the label text string at the right place.
NSMutableAttributedString *atrStr = [[NSAttributedString alloc] initWithString:label.text];
NSNumber *kern = [NSNumber numberWithFloat:0];
NSRange full = NSMakeRange(0, [atrStr string].length);
[atrStr addAttribute:(id)kCTKernAttributeName value:kern range:full];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)atrStr);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, label.frame);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
CFArrayRef lines = CTFrameGetLines(frame);
CTLineRef line = (CTLineRef)CFArrayGetValueAtIndex(lines, label.numberOfLines-1);
CFRange r = CTLineGetStringRange(line);
This gives you the range of the last line of your label text. From there, it's trivial to cut it up and put the ellipsis where you want.
The first part creates an attributed string with the properties it needs to replicate the behavior of UILabel (might not be 100% but should be close enough).
Then we create a framesetter and frame, and get all the lines of the frame, from which we extract the range of the last expected line of the label.
This is clearly some kind of a hack, and as I said if you want complete control over how your text looks you're better off with a pure CoreText implementation of that label.
Ive just written a UILabel extension in Swift 4, using a binary search to speed up the substring calculation
It was originally based on the solution by #paul-slm but has diverged considerably
extension UILabel {
func getTruncatingText(originalString: String, newEllipsis: String, maxLines: Int?) -> String {
let maxLines = maxLines ?? self.numberOfLines
guard maxLines > 0 else {
return originalString
}
guard self.numberOfLinesNeeded(forString: originalString) > maxLines else {
return originalString
}
var truncatedString = originalString
var low = originalString.startIndex
var high = originalString.endIndex
// binary search substring
while low != high {
let mid = originalString.index(low, offsetBy: originalString.distance(from: low, to: high)/2)
truncatedString = String(originalString[..<mid])
if self.numberOfLinesNeeded(forString: truncatedString + newEllipsis) <= maxLines {
low = originalString.index(after: mid)
} else {
high = mid
}
}
// substring further to try and truncate at the end of a word
var tempString = truncatedString
var prevLastChar = "a"
for _ in 0..<15 {
if let lastChar = tempString.last {
if (prevLastChar == " " && String(lastChar) != "") || prevLastChar == "." {
truncatedString = tempString
break
}
else {
prevLastChar = String(lastChar)
tempString = String(tempString.dropLast())
}
}
else {
break
}
}
return truncatedString + newEllipsis
}
private func numberOfLinesNeeded(forString string: String) -> Int {
let oneLineHeight = "A".size(withAttributes: [NSAttributedStringKey.font: font]).height
let totalHeight = self.getHeight(forString: string)
let needed = Int(totalHeight / oneLineHeight)
return needed
}
private func getHeight(forString string: String) -> CGFloat {
return string.boundingRect(
with: CGSize(width: self.bounds.size.width, height: CGFloat.greatestFiniteMagnitude),
options: [.usesLineFragmentOrigin, .usesFontLeading],
attributes: [NSAttributedStringKey.font: font],
context: nil).height
}
}
ResponsiveLabel is a subclass of UILabel which allows to add custom truncation token which responds to touch.
#paul-slm's answer above is what I ended up using, however I found that it is a very intensive process to strip away the last character of a potentially long string one by one until the label fits the required number of lines. Instead it makes more sense to copy over one character at a time from the beginning of the original string to a blank string, until the required number of lines are met. You should also consider not stepping by one character at a time, but by multiple characters at a time, so as to reach the 'sweet spot' sooner. I replaced func getTruncatingText() -> String with the following:
private func getTruncatingText() -> String? {
guard let originalString = originalString else { return nil }
if numberOfLinesNeeded(originalString) > collapsedNumberOfLines {
var truncatedString = ""
var toyString = originalString
while numberOfLinesNeeded(truncatedString + ellipsis) != (collapsedNumberOfLines + 1) {
let toAdd = toyString.startIndex..<toyString.index(toyString.startIndex, offsetBy: 5)
let toAddString = toyString[toAdd]
toyString.removeSubrange(toAdd)
truncatedString.append(String(toAddString))
}
while numberOfLinesNeeded(truncatedString + ellipsis) > collapsedNumberOfLines {
truncatedString.removeSubrange(truncatedString.index(truncatedString.endIndex, offsetBy: -1)..<truncatedString.endIndex)
}
truncatedString += ellipsis
return truncatedString
} else {
return originalString
}
}