I'm using a UITextView which contains multiple colors. I'm using NSMutableAttributedString for this. I'm fetching the words by tapping on the word inside the TextView in console. Now I want to fetch the color of the attributed string along with the word. I've found no resource in Swift. Everything I found was in Objective-C. Here is my UITapGestureRecognizer handler function.
#objc func myMethodToHandleTap(sender: UITapGestureRecognizer) {
let textView = sender.view as! UITextView
let location: CGPoint = sender.location(in: textView)
let position: CGPoint = CGPoint(x: location.x, y: location.y)
let tapPosition: UITextPosition? = textView.closestPosition(to: position)
if tapPosition != nil {
let textRange: UITextRange? = textView.tokenizer.rangeEnclosingPosition(tapPosition!, with: UITextGranularity.word, inDirection: UITextDirection(rawValue: 1))
if textRange != nil && textColor == UIColor.red
{
let tappedWord: String? = textView.text(in: textRange!)
print("Color : RED , tapped word : ", tappedWord!)
}
else if textRange != nil && textColor == UIColor.green
{
let tappedWord: String? = textView.text(in: textRange!)
print("Color : GREEN , tapped word : ", tappedWord!)
}
else {
print("empty space")
}
}
}
If you adding word color programmatic than check tap word range and match with you applied range then you can easily find the color of the tapped word.
textColor where you assign a value before compare?
This will help you
Thank you.
You can get the color this way:
if
let tapPosition = tapPosition,
let characterIndex = textView.offset(from: textView.beginningOfDocument, to: tapPosition)
let textColor = textView.attributedText.attribute(.foregroundColor, at: characterIndex, effectiveRange: nil) as? UIColor
{
//Do what you want with text color
}
Related
I am able to get the word on tapping uitextview but I want a sequence of words by tapping one of it. Image
Over here in the image if I click on Australia I can get the name but for New Zealand I am only getting new or zealand. Is there any way I can get New Zealand as a whole word. My code is.
countryTxtView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didReceiveGestureOnText(recognizer:))))
#objc func didReceiveGestureOnText(recognizer: UITapGestureRecognizer) {
guard let textView = recognizer.view as? UITextView else {return}
let point = recognizer.location(in: textView)
guard let detectedWord = getWordAtPosition(point, textView: textView) else {return} print("selected word : (detectedWord)")
}
func getWordAtPosition(_ point: CGPoint, textView: UITextView) -> String?{
if let textPosition = textView.closestPosition(to: point){
if let range = textView.tokenizer.rangeEnclosingPosition(textPosition, with: .sentence, inDirection: UITextDirection(rawValue: 1)){
return textView.text(in: range)
}
}
return nil
}
Hi I wanna add image round dot to some UILabel in my app.
I have code for adding the image. But I don't understand how I could put the image on the start of the UILabel rather than in the end of the label.
Any suggestion on this? Below is the code I use for it:
What should I add to place image on start of UILabel? I thought imageBehindText :false would fix it but it didn't.
extension UILabel {
/**
This function adding image with text on label.
- parameter text: The text to add
- parameter image: The image to add
- parameter imageBehindText: A boolean value that indicate if the imaga is behind text or not
- parameter keepPreviousText: A boolean value that indicate if the function keep the actual text or not
*/
func addTextWithImage(text: String, image: UIImage, imageBehindText: Bool, keepPreviousText: Bool) {
let lAttachment = NSTextAttachment()
lAttachment.image = image
// 1pt = 1.32px
let lFontSize = round(self.font.pointSize * 1.20) // rounded dot should be smaller than font
let lRatio = image.size.width / image.size.height
lAttachment.bounds = CGRect(x: 0, y: ((self.font.capHeight - lFontSize) / 2).rounded(), width: lRatio * lFontSize, height: lFontSize)
let lAttachmentString = NSAttributedString(attachment: lAttachment)
if imageBehindText {
let lStrLabelText: NSMutableAttributedString
if keepPreviousText, let lCurrentAttributedString = self.attributedText {
lStrLabelText = NSMutableAttributedString(attributedString: lCurrentAttributedString)
lStrLabelText.append(NSMutableAttributedString(string: text))
} else {
lStrLabelText = NSMutableAttributedString(string: text)
}
lStrLabelText.append(lAttachmentString)
self.attributedText = lStrLabelText
} else {
let lStrLabelText: NSMutableAttributedString
if keepPreviousText, let lCurrentAttributedString = self.attributedText {
lStrLabelText = NSMutableAttributedString(attributedString: lCurrentAttributedString)
lStrLabelText.append(NSMutableAttributedString(attributedString: lAttachmentString))
lStrLabelText.append(NSMutableAttributedString(string: text))
} else {
lStrLabelText = NSMutableAttributedString(attributedString: lAttachmentString)
lStrLabelText.append(NSMutableAttributedString(string: text))
}
self.attributedText = lStrLabelText
}
}
I got it to work. The problem was that I was setting the text in the storyboard(.xib). So this extension didn't change the image to the front even if the bool-val were false.
Simply set the text from the function-call and the 'false' value will trigger the image to be set in the start of the uilabel.
Example1 (what I did wrong):
// This is what I tried first!
label.addTextWithImage(text: "",
image: UIImage(named: embededIcon)!,
imageBehindText: false, // note! This is false.
keepPreviousText: true) // this was the problem!
Example2 (what got it to work!):
label.addTextWithImage(text: "putYourLabelTextHere!", // You have to put text here, even if it's already in storyboard.
image: UIImage(named: embededIcon)!,
imageBehindText: false,
keepPreviousText: false) // false, so the image will be set before text!
lazy var attachment: NSTextAttachment = {
let attachment = NSTextAttachment()
attachment.image = UIImage(named: "")
return attachment
}()
it will work for both remote and local image
if let imageUrl = imageUrl, imageUrl.count > 0, let url = URL(string: imageUrl) {
let imageAttachmentString = NSAttributedString(attachment: attachment)
attachment.bounds = CGRect(x: 0, y: (titleLabel.font.capHeight - 16) / 2, width: 16, height: 16)
BaseAPI().downloadImage(with: url, placeHolder: "") { [weak self] image, error, success, url in
if let image = image, let self = self {
DispatchQueue.main.async {
let finalString = NSMutableAttributedString(string: "")
let titleString = NSMutableAttributedString(string: self.title)
let space = NSMutableAttributedString(string: " ")
let imageAttachment = self.attachment
imageAttachment.image = image
finalString.append(imageAttachmentString)
finalString.append(space)
finalString.append(titleString)
self.titleLabel.attributedText = finalString
}
}
}
} else {
titleLabel.text = title
}
I am trying to extract the word that was tapped in a UILabel using tap gesture recognizer. I am able to get the character that was tapped, as well as the range of the character, but I am not able to extract the full word. Is there a way to get the full string of the word that was tapped instead of the character?
if let label = descriptionLabel, tapGestureRecognizer != nil {
if label.attributedText == nil {
return
}
let storage = NSTextStorage(attributedString: label.attributedText ?? NSAttributedString())
let textContainer = NSTextContainer(size: label.bounds.size)
let layoutManager = NSLayoutManager()
layoutManager.addTextContainer(textContainer)
storage.addLayoutManager(layoutManager)
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = (label.lineBreakMode)
textContainer.maximumNumberOfLines = label.numberOfLines
let location: CGPoint = tapGestureRecognizer!.location(in: label)
let characterIndex: Int = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
if characterIndex < storage.length {
print("Character Index: \(Int(characterIndex))")
let range = NSRange(location: characterIndex, length: 1)
let substring: String? = (label.attributedText?.string as NSString?)?.substring(with: range)
//prints out one character, how do I get the full word of this character
print(substring)
}
}
Much appreciated
I don't like this way, but it works. Place the following after the print(substring) statement. Why doesn't UIKit have a method to extract the full string at a Range? Ridiculous.
let afterWords: [Any] = (afterString.components(separatedBy: " "))
let beforeWords: [Any] = (beforeString.components(separatedBy: ", "))
var attachmentString = ""
if let stringBeforeLocation = beforeWords.last as? String, let stringAfterLocation = afterWords.first as? String {
attachmentString = stringBeforeLocation
if stringAfterLocation.count > 0 {
let choppedString = (stringAfterLocation as NSString).substring(from: 1)
attachmentString = attachmentString + choppedString
print("FULL STRING TAPPED \(attachmentString)")
}
}
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 have a string (for example: "This is some text with specific http://goo.gl/45hz web adress in it").
I need to find all URLs within the string if any and then convert somehow the string so the address will be tapable (safari will open if user taps on it) and i need to display the whole string with URL in label idealy (if possible).
Does anybody know how can be this achieved?
In order to find all URLS in a string
NSError *error = NULL;
NSString *string = #"This is some text with specific http://goo.gl/45hz web adress in it";
NSDataDetector *detector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:&error];
NSArray *matches = [detector matchesInString:string
options:0
range:NSMakeRange(0, [string length])];
for (NSTextCheckingResult *match in matches) {
if ([match resultType] == NSTextCheckingTypeLink) {
NSURL *url = [match URL];
NSLog(#"url...%#", url);
}
}
You can use NSRange to find the http:// first and and from that location, discard the text before http://, finally you can separate the remaining string using spaces, take first part of the remaining string which will contain URL.
NSString *givenStr = #"This is some text with specific http://goo.gl/45hz web address in it";
NSRange range = [givenStr rangeOfString:#"http://" options:NSCaseInsensitiveSearch];
if (range.location != NSNotFound) {
NSString *urlString = [givenStr substringFromIndex:range.location];
// urlString="http://goo.gl/45hz web address in it"
NSArray *urlStrArray = [urlString componentsSeparatedByString:#" "];
NSURL *finalURL=[NSURL URLWithString:[urlStrArray objectAtIndex:0]];
// [urlStrArray objectAtIndex:0]="http://goog.gl/45hz"
}
For making URL clickable as said by #calampunay you should use UITextView instead of UILabel because UILabel presents only plain text.
You can use a UITextView instead of UILabel. Set editable to NO and then modify the property #property(nonatomic) UIDataDetectorTypes dataDetectorTypes to detect URLs.
#In swift 5 you will try like this way to find URL
let input = "here is your String"
let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
let matches = detector.matches(in: input, options: [], range:NSRange(location: 0, length: input.utf16.count))
for match in matches {
guard let range = Range(match.range, in: input) else { continue }
let url = input[range]
print(url)
}
Use TTTAttributedLabel if this is coming from a UI label - https://github.com/mattt/TTTAttributedLabel
Works great for us.
I have implemented same feature in swift 3. Below is the complete code to detect url in string and make it tapable :
//put these lines of code inside your function
//add Tap Gesture
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapLabel))
myLabel?.addGestureRecognizer(tapGesture)
let input = “tap on url http://www.google.com, to go to google search”
myLabel?.attributedText = getAttributedString(input: input);
/*** functions to perform complete task ***/
//function to get attributed string with url
func getAttributedString(input : String) -> NSMutableAttributedString {
let matches = getURLRange(input: input)
let attributedString = NSMutableAttributedString(string:input)
for match in matches {
let url = (input as NSString).substring(with: match.range)
print(url)
let linkText = NSMutableAttributedString(string:url, attributes:[NSForegroundColorAttributeName : UIColor.blue] as [String : Any])
attributedString.replaceCharacters(in: match.range, with: linkText)
}
return attributedString
}
//function to get urls range
func getURLRange(input : String) -> [NSTextCheckingResult] {
let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
let matches = detector.matches(in: input, options: [], range: NSRange(location: 0, length: input.utf16.count))
return matches
}
// function to check if URL is taped
func tapLabel(gesture: UITapGestureRecognizer) {
let matches = getURLRange(input: (myLabel?.text)!)
let tapLocation = gesture.location(in: myLabel)
let indexOfCharacter = didTapAttributedTextInLabel(label: myLabel!, tapLocation: tapLocation)
for match in matches {
if NSLocationInRange(indexOfCharacter, match.range) {
//open selected URL
let mainText = myLabel?.text as NSString?
let urlToOpen = URL(string: (mainText?.substring(with: match.range))!)
UIApplication.shared.open(urlToOpen!)
break;
}else {
print("Tapped none")
}
}
}
//function to get index Of selected Character in string
func didTapAttributedTextInLabel(label: UILabel, tapLocation: CGPoint) -> Int {
//here myLabel is the object of UILabel
//added this from #warly's answer
//set font of attributedText
let attributedText = NSMutableAttributedString(attributedString: myLabel!.attributedText!)
attributedText.addAttributes([NSFontAttributeName: myLabel!.font], range: NSMakeRange(0, (myLabel!.attributedText?.string.characters.count)!))
// Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: CGSize(width: (myLabel?.frame.width)!, height: (myLabel?.frame.height)!+100))
let textStorage = NSTextStorage(attributedString: attributedText)
// Configure layoutManager and textStorage
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
// Configure textContainer
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = myLabel!.lineBreakMode
textContainer.maximumNumberOfLines = myLabel!.numberOfLines
let labelSize = myLabel!.bounds.size
textContainer.size = labelSize
// get the index of character where user tapped
let indexOfCharacter = layoutManager.characterIndex(for: tapLocation, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
return indexOfCharacter
}
It is tested and working fine inside my app.
Swift 4 implementation of Ankur's answer:
let messageLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.font = UIFont.systemFont(ofSize: 14, weight: UIFont.Weight.regular)
label.isUserInteractionEnabled = true
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.addSubview(messageLabel)
// setup messageLabel constraints
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapLabel))
messageLabel.addGestureRecognizer(tapGesture)
}
//function to get attributed string with url
func getAttributedString(input : String) -> NSMutableAttributedString {
let matches = getURLRange(input: input)
let attributedString = NSMutableAttributedString(string:input)
for match in matches {
let url = (input as NSString).substring(with: match.range)
let linkText = NSMutableAttributedString(string:url, attributes:[NSAttributedString.Key(rawValue: NSAttributedString.Key.foregroundColor.rawValue) : UIColor.appleBlue()])
attributedString.replaceCharacters(in: match.range, with: linkText)
}
return attributedString
}
func getURLRange(input : String) -> [NSTextCheckingResult] {
let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
let matches = detector.matches(in: input, options: [], range: NSRange(location: 0, length: input.utf16.count))
return matches
}
//function to get index Of selected Character in string
func didTapAttributedTextInLabel(label: UILabel, tapLocation: CGPoint) -> Int {
//here myLabel is the object of UILabel
//added this from #warly's answer
//set font of attributedText
let attributedText = NSMutableAttributedString(attributedString: messageLabel.attributedText!)
attributedText.addAttributes([NSAttributedString.Key.font: messageLabel.font], range: NSMakeRange(0, (messageLabel.attributedText?.string.count)!))
// Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: CGSize(width: messageLabel.frame.width, height: messageLabel.frame.height+100))
let textStorage = NSTextStorage(attributedString: attributedText)
// Configure layoutManager and textStorage
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
// Configure textContainer
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = messageLabel.lineBreakMode
textContainer.maximumNumberOfLines = messageLabel.numberOfLines
let labelSize = messageLabel.bounds.size
textContainer.size = labelSize
// get the index of character where user tapped
let indexOfCharacter = layoutManager.characterIndex(for: tapLocation, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
return indexOfCharacter
}
// function to check if URL is taped
#objc func tapLabel(gesture: UITapGestureRecognizer) {
let matches = getURLRange(input: (messageLabel.text)!)
let tapLocation = gesture.location(in: messageLabel)
let indexOfCharacter = didTapAttributedTextInLabel(label: messageLabel, tapLocation: tapLocation)
for match in matches {
if NSLocationInRange(indexOfCharacter, match.range) {
//open selected URL
let mainText = messageLabel.text as NSString?
if let urlToOpen = URL(string: (mainText?.substring(with: match.range))!) {
UIApplication.shared.open(urlToOpen)
} else {
print("We have error with URL")
}
break
} else {
print("Tapped none")
}
}
}
Swift 5 extension
extension String {
func extractUrl() -> URL? {
let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
let matches = detector.matches(in: self, options: [], range: NSRange(location: 0, length: self.utf16.count))
for match in matches {
guard let range = Range(match.range, in: self) else { continue }
let url = self[range]
return URL(string: String(url))
}
return nil
}
}