This question already has answers here:
Create tap-able "links" in the NSAttributedString of a UILabel?
(37 answers)
Closed 6 years ago.
I have an attributed string UILabel, I was able to color some parts of the text
let text = "Why did \(event) give \(event2) a 5 stars review? Details here. "
let linkTextWithColor = "Why did"
let range = (text as NSString).rangeOfString(linkTextWithColor)
let attributedString = NSMutableAttributedString(string:text)
attributedString.addAttribute(NSForegroundColorAttributeName, value: UIColor.blackColor() , range: range)
labelEvent.attributedText = attributedString
Now I want to make some parts of the text tappable, like a UIButton, how to do this ?
Example 1
Example 2
I need to have blue text to be responding to touch, and to run a specific function, like a UIButton.
help is really appreciated, thanks.
What you want to do is use an attributed string with a text view to make a link that will act as a button.
let attributedString = NSMutableAttributedString(string: "Some string with link", attributes: [<attributes>])
Then set a part of it as a link, and customize it's appearance using the linkAttributes property of the Text View. Because this is a button and not an actual link we just put a dummy url in for the link so we can handle it in our delegate later.
attributedString.setSubstringAsLink(substring: "link", linkURL: "CUSTOM://WHATEVER")
let linkAttributes: [String : AnyObject] = [NSForegroundColorAttributeName : .redColor(), NSUnderlineColorAttributeName : .redColor(), NSUnderlineStyleAttributeName : NSUnderlineStyle.StyleSingle.rawValue]
textView.linkTextAttributes = linkAttributes
textView.attributedText = attributedString
textView.selectable = true
textView.editable = false
textView.userInteractionEnabled = true
Finally in text view delegate we will check for the scheme and perform some action.
func textView(textView: UITextView, shouldInteractWithURL URL: NSURL, inRange characterRange: NSRange) -> Bool {
if URL.scheme == "CUSTOM" {
// Do your button actions here
}
return true
}
Extension for setSubstringAsLink:
extension NSMutableAttributedString {
// Set part of string as URL
public func setSubstringAsLink(substring substring: String, linkURL: String) -> Bool {
let range = self.mutableString.rangeOfString(substring)
if range.location != NSNotFound {
self.addAttribute(NSLinkAttributeName, value: linkURL, range: range)
return true
}
return false
}
}
Let's say you have a UILabel with text "abc123", and you want "abc" to function as a UIButton.
Calculate and store the rectangle that contains "abc".
Add a UITapGestureRecognizer to the UILabel.
When the UILabelis tapped, check if the tap is within the rectangle.
static func getRect(str: NSAttributedString, range: NSRange, maxWidth: CGFloat) -> CGRect {
let textStorage = NSTextStorage(attributedString: str)
let textContainer = NSTextContainer(size: CGSize(width: maxWidth, height: CGFloat.max))
let layoutManager = NSLayoutManager()
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
textContainer.lineFragmentPadding = 0
let pointer = UnsafeMutablePointer<NSRange>.alloc(1)
layoutManager.characterRangeForGlyphRange(range, actualGlyphRange: pointer)
return layoutManager.boundingRectForGlyphRange(pointer.move(), inTextContainer: textContainer)
}
let rect1 = getRect(label.attributedText!, range: NSMakeRange(0, 3), maxWidth: label.frame.width)
label.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(MyClass.tappedLabel(_:))))
func tappedLabel(sender: UITapGestureRecognizer) {
if rect1.contains(sender.locationInView(sender.view)) {
// ...
}
}
I recommend you try this library out
https://github.com/null09264/FRHyperLabel
Great library, easy to use and has few built in examples for you to try out. Examples are in both Objective-c and Swift
Example in Swift
let str = "This is a random bit of text"
let attributes = [NSForegroundColorAttributeName: UIColor.blackColor(),
NSFontAttributeName: UIFont.systemFontOfSize(15)]
confirmLabel.attributedText = NSAttributedString(string: str, attributes: attributes)
let handler = {
(hyperLabel: FRHyperLabel!, substring: String!) -> Void in
//action here
}
//Step 3: Add link substrings
confirmLabel.setLinksForSubstrings(["random"], withLinkHandler: handler)
Edit:
If you want to get rid of the underline, best way to do this is to follow the advice that DeyaEldeen gave in the comment.
If you go to the .m file of FRHyperLabel, go to this method
- (void)checkInitialization {
if (!self.handlerDictionary) {
self.handlerDictionary = [NSMutableDictionary new];
}
if (!self.userInteractionEnabled) {
self.userInteractionEnabled = YES;
}
if (!self.linkAttributeDefault) {
self.linkAttributeDefault = #{NSForegroundColorAttributeName: FRHyperLabelLinkColorDefault,
NSUnderlineStyleAttributeName: #(NSUnderlineStyleSingle)};
}
if (!self.linkAttributeHighlight) {
self.linkAttributeHighlight = #{NSForegroundColorAttributeName: FRHyperLabelLinkColorHighlight,
NSUnderlineStyleAttributeName: #(NSUnderlineStyleSingle)};
}
}
And you can just remove this
NSUnderlineStyleAttributeName: #(NSUnderlineStyleSingle)
from the attributes
The idea is to use library such as TTTAttributedLabel and using it this way :
Consider the following string "I want to be touchable here" where here will perform a segue on touch.
Create TTTAttributedLabel (UILabel subclass), and put text content in it, as if it was a UILabel. Set its delegate to self. Then, add a link to a word this way :
Objective C
NSRange rangeWord = [attributedLabel.text rangeOfString:#"here"];
[attributedLabel addLinkToURL:[NSURL URLWithString:#"anActionOnClickHere"] withRange:rangeUser];
Swift
NSRange rangeWord = attributedLabel.text.rangeOfString("here")
attributedLabel.addLinkToURL(NSURL(string: "anActionOnClickHere"), withRange:rangeUser)
On clicking words it will call this method in which you can handle the click :
Objective C
- (void)attributedLabel:(__unused TTTAttributedLabel *)label
didSelectLinkWithURL:(NSURL *)url {
NSString *urlToString = [url absoluteString];
if ([urlToString containsString:#"anActionOnClickHere"]) { //perform segue for example
[self performSegueWithIdentifier:#"hereSegue" sender:self];
}
}
Swift
func attributedLabel(label: TTTAttributedLabel!, didSelectLinkWithURL url: NSURL!) {
NSString *urlToString = url.absoluteString()
if (urlToString.containsString("anActionOnClickHere")) { //perform segue for example
self.performSegueWithIdentifier("hereSegue",sender:self)
}
}
With the default link style, you'll get the blue color that you're looking for.
Related
I'm trying to implement an editor that can handle hashtag while typing.
extension UITextView {
func resolveHashTags() {
if self.text.isEmpty {
let emptyString = NSMutableAttributedString(string: " ", attributes: [NSAttributedString.Key.foregroundColor: UIColor.black,
NSAttributedString.Key.font: self.font!])
self.attributedText = emptyString
self.textColor = .black
self.text = ""
return
}
let cursorRange = selectedRange
let nsText = NSString(string: self.text)
let words = nsText.components(separatedBy: CharacterSet(charactersIn: "##ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_").inverted).filter({!$0.isEmpty})
self.textColor = .black
let attrString = NSMutableAttributedString()
attrString.setAttributedString(self.attributedText)
attrString.addAttributes([NSAttributedString.Key.foregroundColor : UIColor.black], range: nsText.range(of: self.text))
var anchor: Int = 0
for word in words {
// found a word that is prepended by a hashtag!
// homework for you: implement #mentions here too.
let matchRange:NSRange = nsText.range(of: word as String, range: NSRange(location: anchor, length: nsText.length - anchor))
anchor = matchRange.location + matchRange.length
if word.hasPrefix("#") {
// a range is the character position, followed by how many characters are in the word.
// we need this because we staple the "href" to this range.
// drop the hashtag
let stringifiedWord = word.dropFirst()
if let firstChar = stringifiedWord.unicodeScalars.first, NSCharacterSet.decimalDigits.contains(firstChar) {
// hashtag contains a number, like "#1"
// so don't make it clickable
} else {
// set a link for when the user clicks on this word.
// it's not enough to use the word "hash", but you need the url scheme syntax "hash://"
// note: since it's a URL now, the color is set to the project's tint color
attrString.addAttribute(NSAttributedString.Key.link, value: "hash:\(stringifiedWord)", range: matchRange)
}
} else if !word.hasPrefix("#") {
}
}
self.attributedText = attrString
self.selectedRange = cursorRange
}
}
So this is the extension I'm using to create a hyperlink in UITextView. Called in func textViewDidChange(_ textView: UITextView)
So while typing if any word starts with #. It'll turn in hyperlinks and will change color to blue. After typing the intended word if you press space it goes back to black text. This is expected behavior.
But if you clear text and move your course back to hashtag word like this
it keeps extending hyperlink to the next word too.
any solution to keep hyperlinks to that word only. Anything typed after hashtag should be normal text
I finally figured it out.
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
var shouldReturn = true
let selectedRange = textView.selectedRange
let attributedText = NSMutableAttributedString(attributedString: textView.attributedText)
if !text.isEmpty && text != " " {
var userAttributes = [(NSAttributedString.Key, Any, NSRange)]()
attributedText.enumerateAttribute(.link, in: _NSRange(location: 0, length: textView.text.count), options: .longestEffectiveRangeNotRequired) { (value, range, stop) in
if let url = value as? String, url.hasPrefix("user:") {
userAttributes.append((.link, value!, range))
}
}
if let userLink = userAttributes.first(where: {$0.2.contains(range.location - 1)}) {
attributedText.replaceCharacters(in: range, with: NSAttributedString(string: text, attributes: [NSAttributedString.Key.link : userLink.1, NSAttributedString.Key.font : textView.font as Any]))
textView.attributedText = attributedText
shouldReturn = false
} else {
attributedText.replaceCharacters(in: range, with: NSAttributedString(string: text, attributes: [NSAttributedString.Key.font : textView.font as Any]))
textView.attributedText = attributedText
textDidChange?(textView)
shouldReturn = false
}
textView.selectedRange = _NSRange(location: selectedRange.location + text.count, length: 0)
textViewDidChange(textView)
}
return shouldReturn
}
This way I have the control to update the link in between the word and it doesn't extend afterward to a new word.
I looked around SO and couldn't find this exact problem, despite there being a few questions with similar titles.
All I want to do is have some matching text on UILabel be drawn in BOLD. I'm using it when I'm searching for objects, it should 'bolden' the search term. To this aim, I wrote the following code:
extension String {
func boldenOccurrences(of searchTerm: String?, baseFont: UIFont, textColor: UIColor) -> NSAttributedString {
let defaultAttributes: [String : Any] = [NSForegroundColorAttributeName : textColor,
NSFontAttributeName: baseFont]
let result = NSMutableAttributedString(string: self, attributes: defaultAttributes)
guard let searchTerm = searchTerm else {
return result
}
guard searchTerm.characters.count > 0 else {
return result
}
// Ranges. Crash course:
//let testString = "Holy Smokes!"
//let range = testString.startIndex ..< testString.endIndex
//let substring = testString.substring(with: range) // is the same as testString
var searchRange = self.startIndex ..< self.endIndex //whole string
var foundRange: Range<String.Index>!
let boldFont = UIFont(descriptor: baseFont.fontDescriptor.withSymbolicTraits(.traitBold)!, size: baseFont.pointSize)
repeat {
foundRange = self.range(of: searchTerm, options: .caseInsensitive , range: searchRange)
if let found = foundRange {
// now we have to do some stupid stuff to make Range compatible with NSRange
let rangeStartIndex = found.lowerBound
let rangeEndIndex = found.upperBound
let start = self.distance(from: self.startIndex, to: rangeStartIndex)
let length = self.distance(from: rangeStartIndex, to: rangeEndIndex)
log.info("Bolden Text: \(searchTerm) in \(self), range: \(start), \(length)")
let nsRange = NSMakeRange(start, length)
result.setAttributes([NSForegroundColorAttributeName : textColor,
NSFontAttributeName: boldFont], range: nsRange)
searchRange = found.upperBound ..< self.endIndex
}
} while foundRange != nil
return result
}
}
Everything "looks" fine. The log statement spits out what I expect and it's all good. However, when drawn on the UILabel, sometimes an entire string is set to bold, and I don't understand how that could be happening. Nothing in the code suggests this should be happening.
I set the result of this above method in a typical UITableCell configuration method (i.e. tableView(cellForRowAt indexPath:.... ) )
cell.titleLabel.attributedText = artist.displayName.emptyIfNil.boldenOccurrences(of: source.currentSearchTerm, baseFont: cell.titleLabel.font, textColor: cell.titleLabel.textColor)
Your primary issue is the cell reuse, maybe when a cell is reused keep your font bold as font, and that is why you have this issue, you can solve this in your cell prepareForReuse() method you can add
override func prepareForReuse() {
super.prepareForReuse()
//to fix ipad Error
self.titleLabel.font = UIFont(name: "YourBaseFont", size: yourFontSize)
}
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
Download an example I have created
Example Link
I am adding a link to my UITextView like this:
let systemFont = self.text.font!
let linkAttributes = [
NSFontAttributeName : systemFont,
NSLinkAttributeName: NSURL(string: alertController.textFields![0].text!)!] as [String : Any]
let myAttributes2 = [ NSForegroundColorAttributeName: customGreen]
let attributedString = NSMutableAttributedString(string: "\(urlName)")
// Set the 'click here' substring to be the link
attributedString.setAttributes(linkAttributes, range: NSMakeRange(0, urlName.characters.count))//(0, urlName.characters.count))
self.text.linkTextAttributes = myAttributes2
self.text.textStorage.insert(attributedString, at: self.text.selectedRange.location)
let cursor = NSRange(location: self.text.selectedRange.location + "\(urlName)".characters.count, length: 0)
self.text.selectedRange = cursor
// self.text.font = systemFont
But after inserting it, it changes all the current text in the UITextView to the same font.
So for example if I have some text that is Bold and some more text that is Italic, after I add the url(which is system font) it resets all the bold/italic text to the system font....
If anyone knows how to keep the current font of the previous text I'd really appreciate the help.
For any further explanation just drop a comment below!
Many thanks in advance to anyone that can help!
Update
I am changing the text here in textDidChange
if text == " "
{
// let systemFont = self.text.font!
// let linkAttributes = [NSFontAttributeName : systemFont]
let attributes = [NSForegroundColorAttributeName: UIColor.black, NSFontAttributeName: self.text.font!] as [String : Any]
let attributedString = NSMutableAttributedString(string: text, attributes: attributes)
self.text.textStorage.insert(attributedString, at: range.location)
let cursor = NSRange(location: self.text.selectedRange.location+1, length: 0)
textView.selectedRange = cursor
return false
}
I have that so after adding the URL I make a space and reset the font so I don't continue typing as a URL link...Probably the problem but when I type normal and don't set a url link the text doesn't changing while make a space...
let urlName = "google"
#IBAction func btnPressed(_ sender: Any) {
let systemFont = UIFont.systemFont(ofSize: 11)
let linkAttributes = [
NSFontAttributeName : systemFont,
NSLinkAttributeName: NSURL(string: "https://www.google.com")!] as [String : Any]
let myAttributes2 = [NSForegroundColorAttributeName: UIColor.green]
let attributedString = NSMutableAttributedString(string: "\(urlName)")
attributedString.setAttributes(linkAttributes, range: NSMakeRange(0, urlName.characters.count))//(0, urlName.characters.count))
self.text.linkTextAttributes = myAttributes2
self.text.textStorage.insert(attributedString, at: self.text.selectedRange.location)
let cursor = NSRange(location: self.text.selectedRange.location + "\(urlName)".characters.count, length: 0)
self.text.selectedRange = cursor
}
This is how you should update the textView if you want to add text programmatically.
Do not use the textViewDidChange(_:) delegate method in this situation.
UPDATE
I've downloaded your code from dropbox and made some changes to it.
so here I'm changing the textView text in viewDidLoad() for example.
I've created two constants in order to use them again in the code, fontAttr is the original font with the name and size and fontColorAttrBlue is the original font color.
let fontAttr = UIFont(name: "Helvetica-Bold", size: 20)
let fontColorAttrBlue = UIColor.blue
override func viewDidLoad() {
super.viewDidLoad()
let attrString = NSMutableAttributedString(string: "Hello World, How are you?", attributes: [NSFontAttributeName : fontAttr!, NSForegroundColorAttributeName: fontColorAttrBlue ])
self.textView.delegate = self // you can set the delegate from the storyboard.
self.textView.attributedText = attrString
}
And I've deleted this line of code self.textView.font = systemFont from webLink(_ sender: AnyObject) action method, so that it wouldn't change the font of the textView.
And lastly in textView(_:shouldChangeTextIn:replacementText:) method instead of checking if the user has entered " " String I'm checking if the user has entered any String and reusing the font attributes that I created in the beginning, so that if the user enters any kind of text after the link it would be the original text and not the one assigned for the like text.
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if text.characters.count > 0 {
let attributedString = NSMutableAttributedString(string: text, attributes: [NSFontAttributeName : self.fontAttr!, NSForegroundColorAttributeName: self.fontColorAttrBlue])
self.textView.textStorage.insert(attributedString, at: range.location)
let cursor = NSRange(location: self.textView.selectedRange.location+1, length: 0)
textView.selectedRange = cursor
return false
}
return true
}
I have a textView in my tableViewCell. In the Interfacebuilder I set a link, mail and address detection for the textview. So all the links, mail and addresses are highlighted. The textview is selectable, too.
I'm working with an autorefresh, so the content of the the tableViewCell will reload all 30 seconds. Everytime when this is happen, the highlighting disappears for < 1 second and the the highlighting comes back.
Sometimes this happens to at the initialize load of the tableViewCell.
There seems to be a bunch of bugs in iOS 7... but I use minimum 8.4.
So does anybody know this bug oder has some help? Thank you
Try to use the attribute method to customize your string in a method similar to this:
let myAttribute = [ NSForegroundColorAttributeName: UIColor.blueColor() ]
let myAttrString = NSAttributedString(string: stringWithLinks, attributes: myAttribute)
So you have your customized string.
Good job
#Carlo
This example doesn't work for me.
Important: In the Interfacebuilder the textview has to be selectable = true/on
What worked for me is my own parser to get all links/mails (whatever you want):
func searchForLinksAndMailsInString(remark: String) -> NSMutableAttributedString {
let attributedString = NSMutableAttributedString(string: remark)
//set default color for non links and mails
attributedString.addAttribute(NSForegroundColorAttributeName, value: UIColor.whiteColor(), range: NSRange(location:0,length:remark.characters.count))
//search for valid websites
let matchesForWebsite = Model.sharedInstance().parseForRegexInText(RegexFilter.regexFindAllValidWebsites, text: remark)
//make links clickable
for match in matchesForWebsite {
attributedString.setAsLink(match, linkURL: match)
}
//same as websites just for mails
let matchesForMails = Model.sharedInstance().parseForRegexInText(RegexFilter.regexFindAllValidMails, text: remark)
for match in matchesForMails {
attributedString.setAsLMail(match, mail: match)
}
return attributedString
}
Extension to make the results clickable
extension NSMutableAttributedString {
//not my work see [here][1]
public func setAsLink(textToFind:String, linkURL:String) -> Bool {
let foundRange = self.mutableString.rangeOfString(textToFind)
if foundRange.location != NSNotFound {
if let linkURLToURL = NSURL(string: linkURL) {
self.addAttribute(NSLinkAttributeName, value: linkURLToURL, range: foundRange)
}
return true
}
return false
}
public func setAsLMail(textToFind:String, mail:String) -> Bool {
let foundRange = self.mutableString.rangeOfString(textToFind)
if foundRange.location != NSNotFound {
if let mailToURL = NSURL(string: "mailto:\(mail)") {
self.addAttribute(NSLinkAttributeName, value: mailToURL, range: foundRange)
}
return true
}
return false
}
}
How to call:
let attributedString = searchForLinksAndMailsInString(remark)
subtitleTextView.attributedText = attributedString
My regexs (they work, no guarantee for elegance):
class RegexFilter {
static let regexFindAllValidMails = "[A-Z0-9a-z._%+-]+#[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
static let regexFindAllValidWebsites = "(((http(s)?://)(www.)?)|(www.))([a-z][a-z0-9]*).([a-z]{2,3})"
}