Hyperlink / URL inside textView paragraph - ios

I have a paragraph which is a textView. I want to put hyperlinks inside some of the words in the paragraph.
var attributedString = NSMutableAttributedString(string: text)
attributedString.addAttribute(NSLinkAttributeName, value: hyperlink, range: range)
var linkAttributes = [NSForegroundColorAttributeName: UIColor.blueColor(),
NSUnderlineStyleAttributeName: 1
]
textView!.linkTextAttributes = linkAttributes
textView!.attributedText = attributedString
textView!.delegate = self
UITextViewDelegate
func textView(textView: UITextView, shouldInteractWithURL URL: NSURL, inRange characterRange: NSRange) -> Bool {
if UIApplication.sharedApplication().canOpenURL(URL) {
return true
} else {
CozyStyles.alert(title: "Sorry", message: (URL.scheme!).capitalizeFirst + " is not installed", action: "OK")
return false
}
}
This approach works but it doesn't work good enough. When simple taped it doesn't recognise the textView tap. The link must be long pressed to make it work which isn't user friendly.
Is there a work around for this?
This solution also doesn't work since I have another tap gesture.

An additional tap gesture solution similar to the one in the link you've provided should work, even if you currently have another tap gesture.
If your textView is at the top of the view hierarchy then an additional gesture recognizer added to the textView should just recognize the tap.
If your existing tap gesture is added to a view which is on top of your textView, you can implement UIGestureRecognizerDelegate's shouldReceiveTouch method and handle a tap on the textView in case you hit it, while denying the gesture from receiving the touch.
If that's the case then you might not even need an additional gesture:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
CGPoint location = [touch locationInView:self.view];
if (CGRectContainsPoint(self.textView.frame, location)) {
[self handleTapOnTextViewAtLocation:[self.view convertPoint:location toView:self.textView]];
return NO;
}
return YES;
}
- (void)handleTapOnTextViewAtLocation:(CGPoint)location {
UITextPosition *textPosition = [self.textView closestPositionToPoint:location];
NSDictionary *textStyling = [self.textView textStylingAtPosition:textPosition inDirection:UITextStorageDirectionForward];
NSURL *url = textStyling[NSLinkAttributeName];
if (url) {
NSLog(#"url tapped: %#", url);
}
}

Use this code:
textView.dataDetectorTypes = UIDataDetectorTypeLink;

Related

UITextView Delayed AttributedString Link SwiftUI

I have a Text view thal look like this:
class StudyText: UITextView, UITextViewDelegate {
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
print(URL)
return false
}
override var canBecomeFirstResponder: Bool {
return false
}
}
and this is the struct:
struct ClickableText: UIViewRepresentable {
#Binding var text: NSMutableAttributedString
func makeUIView(context: Context) -> StudyText {
let view = StudyText()
view.dataDetectorTypes = .all
view.isEditable = false
view.isSelectable = true
view.delegate = view
view.isUserInteractionEnabled = true
return view
}
func updateUIView(_ uiView: StudyText, context: Context) {
uiView.attributedText = text
}
}
And I am using the attributed links.
Every solution I tried doesn't make the links respond to a quick tap. immediately. It takes a bit of delay until the print statement is presented.
I tried this:
view.delaysContentTouches = false
And I tried this:
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tappedTextView(tapGesture:)))
self.addGestureRecognizer(tapRecognizer)
#objc func tappedTextView(tapGesture: UIGestureRecognizer) {
let textView = tapGesture.view as! UITextView
let tapLocation = tapGesture.location(in: textView)
let textPosition = textView.closestPosition(to: tapLocation)
let attr = textView.textStyling(at: textPosition!, in: .forward)!
if let url: URL = attr[NSAttributedString.Key(rawValue: NSAttributedString.Key.link.rawValue)] as? URL {
print("clicking here: \(url)")
}
}
But none of them worked. It always responds with a delay
How can I fix this?
UITextView responds to both single tap gestures (which let you follow a link) and double tap gestures (which let you select text). Immediately after you tap the link once, it's unclear whether you've completed your gesture or whether a second tap is coming. Only after a short delay with no second tap can it be sure that you were in fact doing a single tap at which point textView(_:shouldInteractWith:in:interaction:) is called.
Unfortunately there's no standard way to make UITextView allow following links without allowing text selection. You might be able to hunt through the gesture recognizers registered on the view and find the one responsible for recognizing double taps and disable it, but doing so could have unintended side effects.

UITextView link tap recognition is delayed

In an iOS 7 app, I have a UITextView with a link in it, but tapping the link doesn't fire. It only responds to an awkward "tap and hold". I want it to respond as soon as a user taps on it, like how a UIWebView link tap works. Here is my setup:
- (void)viewDidLoad
{
[super viewDidLoad];
NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:#"Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."];
[text addAttribute:NSLinkAttributeName value:#"myurl://tapped" range:NSMakeRange(6, 16)];
self.textView.attributedText = text;
self.textView.editable = NO;
self.textView.delaysContentTouches = NO;
}
- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange
{
if ([[URL scheme] isEqualToString:#"myurl"])
{
// Handle tap
return NO;
}
return YES;
}
The Apple Documentation for the shouldInteractWithURL method states: "The text view calls this method if the user taps or long-presses the URL link". The long-press is working, but the tap doesn't seem to work.
Does anyone know how to get this to respond immediately?
If you still want to go with a native UITextView, you can add a tap recognizer to your textview and get the string attributes at the tap location. When you find a link, you can open it immediately.
I wrote a Gist that solves this for iOS 7/8. It's a lightweight extension of UITextView that also forwards -[UITextViewDelegate textView:shouldInteractWithURL:inRange:] and exposes the internal tap gesture recognizer.
https://gist.github.com/benjaminbojko/c92ac19fe4db3302bd28
Here's a quick example:The example below only works on iOS 8. See the gist above for iOS 7 + 8 support.
Add your tap recognizer:
// ...
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tappedTextView:)];
[myTextView addGestureRecognizer:tapRecognizer];
myTextView.selectable = YES; // otherwise the gesture won't recognize
// ...
And add your callback:
- (void)tappedTextView:(UITapGestureRecognizer *)tapGesture {
if (tapGesture.state != UIGestureRecognizerStateEnded) {
return;
}
UITextView *textView = (UITextView *)tapGesture.view;
CGPoint tapLocation = [tapGesture locationInView:textView];
UITextPosition *textPosition = [textView closestPositionToPoint:tapLocation];
NSDictionary *attributes = [textView textStylingAtPosition:textPosition inDirection:UITextStorageDirectionForward];
NSURL *url = attributes[NSLinkAttributeName];
if (url) {
[[UIApplication sharedApplication] openURL:url];
}
}
And swift version:
Tap recognizer:
let tapRecognizer = UITapGestureRecognizer(target: self, action: Selector("tappedTextView:"))
myTextView.addGestureRecognizer(tapRecognizer)
myTextView.selectable = true
Callback:
func tappedTextView(tapGesture: UIGestureRecognizer) {
let textView = tapGesture.view as! UITextView
let tapLocation = tapGesture.locationInView(textView)
let textPosition = textView.closestPositionToPoint(tapLocation)
let attr: NSDictionary = textView.textStylingAtPosition(textPosition, inDirection: UITextStorageDirection.Forward)
if let url: NSURL = attr[NSLinkAttributeName] as? NSURL {
UIApplication.sharedApplication().openURL(url)
}
}
Is the UITextView selectable?. Try with:
self.textView.selectable = YES;
Edit:
I'm starting to think that maybe a long-press is the only way to fire it contrary to what apple says. Check this link, maybe it will help.
As nnarayann mentioned, CCHLinkTextView avoids the problem of delayed tap recognition. This library implements its own gesture recognizer and is now available in version 1.0.
Swift 4 solution
Handles links using tap gesture recognizer
Whether isSelectable is true or false, it works
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleLinkTap(_:)))
textView.addGestureRecognizer(tapRecognizer)
#objc func handleLinkTap(_ recognizer: UITapGestureRecognizer) {
let tapLocation = recognizer.location(in: textView)
guard
let textPosition = textView.closestPosition(to: tapLocation),
let url = textView.textStyling(at: textPosition, in: .forward)?[NSAttributedStringKey.link.rawValue],
let urlString = (url as? String) ?? (url as? URL)?.absoluteString,
urlString == "myurl"
else { return }
let url = URL(string: urlString)!
// Do whatever you want with this URL, such as
UIApplication.shared.openURL(url)
}
Sometimes it's not working on the iOS Simulator or only work if you tap & hold.
You should test it on a real device.
And don't forget to set it selectable.
If there isn't any pressing reason to use a UITextView, such as there being other text being displayed, you could use a UILabel combined with a UITapGestureRecognizer to get the effect you're looking for.
Otherwise, you could go with an actual UIWebView instead.
Have you tried setting textview.delaysContentTouches = NO; ? Maybe that could help.
If you don't need editing, you can use this:
textView.isEditable = true
func textViewShouldBeginEditing(_ textView: UITextView) -> Bool
{
return false
}
Use this method
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}

How to detect touch on NSTextAttachment

What is the best way to detect when user taps on NSTextAttachment on iOS?
I think that one of the ways would be checking for the character on carret's position whether it is NSAttachmentCharacter, but it just doesn't seem right.
I've also tried UITextViewDelegate method: -(BOOL)textView:(UITextView *)textView shouldInteractWithTextAttachment:(NSTextAttachment *)textAttachment inRange:(NSRange)characterRange but it's not invoked when textView.editable=YES
Josh's answer is almost perfect. However, if you tap in the whitespace of your UITextView past the end of the input, glyphIndex(for:in:fractionOfDistanceThroughGlyph) will return the final glyph in the string. If this is your attachment, it will incorrectly evaluate to true.
Apple's docs say: If no glyph is under point, the nearest glyph is returned, where nearest is defined according to the requirements of selection by mouse. Clients who wish to determine whether the the point actually lies within the bounds of the glyph returned should follow this with a call to boundingRect(forGlyphRange:in:) and test whether the point falls in the rectangle returned by that method.
So, here is a tweaked version (Swift 5, XCode 10.2) that performs an additional check on the bounds of the detected glyph. I believe some of the characterIndex tests are now superfluous but they don't hurt anything.
One caveat: glyphs appear to extend to the height of the line containing them. If you have a tall portrait image attachment next to a landscape image attachment, taps on the whitespace above the landscape image will still evaluate to true.
import UIKit
import UIKit.UIGestureRecognizerSubclass
// Thanks to https://stackoverflow.com/a/52883387/658604
// and https://stackoverflow.com/a/49153247/658604
/// Recognizes a tap on an attachment, on a UITextView.
/// The UITextView normally only informs its delegate of a tap on an attachment if the text view is not editable, or a long tap is used.
/// If you want an editable text view, where you can short cap an attachment, you have a problem.
/// This gesture recognizer can be added to the text view, and will add requirments in order to recognize before any built-in recognizers.
class AttachmentTapGestureRecognizer: UITapGestureRecognizer {
typealias TappedAttachment = (attachment: NSTextAttachment, characterIndex: Int)
private(set) var tappedState: TappedAttachment?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
tappedState = nil
guard let textView = view as? UITextView else {
state = .failed
return
}
if let touch = touches.first {
tappedState = evaluateTouch(touch, on: textView)
}
if tappedState != nil {
// UITapGestureRecognizer can accurately differentiate discrete taps from scrolling
// Therefore, let the super view evaluate the correct state.
super.touchesBegan(touches, with: event)
} else {
// User didn't initiate a touch (tap or otherwise) on an attachment.
// Force the gesture to fail.
state = .failed
}
}
/// Tests to see if the user has tapped on a text attachment in the target text view.
private func evaluateTouch(_ touch: UITouch, on textView: UITextView) -> TappedAttachment? {
let point = touch.location(in: textView)
let glyphIndex: Int = textView.layoutManager.glyphIndex(for: point, in: textView.textContainer, fractionOfDistanceThroughGlyph: nil)
let glyphRect = textView.layoutManager.boundingRect(forGlyphRange: NSRange(location: glyphIndex, length: 1), in: textView.textContainer)
guard glyphRect.contains(point) else {
return nil
}
let characterIndex: Int = textView.layoutManager.characterIndexForGlyph(at: glyphIndex)
guard characterIndex < textView.textStorage.length else {
return nil
}
guard NSTextAttachment.character == (textView.textStorage.string as NSString).character(at: characterIndex) else {
return nil
}
guard let attachment = textView.textStorage.attribute(.attachment, at: characterIndex, effectiveRange: nil) as? NSTextAttachment else {
return nil
}
return (attachment, characterIndex)
}
}
Apple make this really difficult. As others point out, the delegate method is called, but only when isEditable is false, or when the user does a tap and hold on the attachment. If you want to be informed about a simple tap interaction during editing, forget it.
I went down the touchesBegan: and hitTest: paths, both with problems. The touches methods are called after the UITextView has already handled the interaction, and the hitTest: is too crude, because it messes with the first responder status and so forth.
My solution in the end was gesture recognizers. Apple are using those internally, which explains why touchesBegan: is not really viable in the first place: the gesture recognizers have already handled the event.
I created a new gesture recognizer class for use with a UITextView. It simply checks for the location of the tap, and if it is an attachment, it handles it. I make all the other gesture recognizers subordinate to my one, so we get first look at the events, and the others only come into play if our one fails.
The gesture recognizer class is below, along with an extension for adding it to UITextView. I add it in my UITextView subclass in awakeFromNib, like this. (You needn't use a subclass if you don't have one.)
override func awakeFromNib() {
super.awakeFromNib()
let recognizer = AttachmentTapGestureRecognizer(target: self, action: #selector(handleAttachmentTap(_:)))
add(recognizer)
and I handle the action by calling the existing UITextViewDelegate method textView(_:,shouldInteractWith:,in:,interaction:). You could just as easily put the handling code directly in the action, rather than using the delegate.
#IBAction func handleAttachmentTap(_ sender: AttachmentTapGestureRecognizer) {
let _ = delegate?.textView?(self, shouldInteractWith: sender.attachment!, in: NSRange(location: sender.attachmentCharacterIndex!, length: 1), interaction: .invokeDefaultAction)
}
Here is the main class.
import UIKit
import UIKit.UIGestureRecognizerSubclass
/// Recognizes a tap on an attachment, on a UITextView.
/// The UITextView normally only informs its delegate of a tap on an attachment if the text view is not editable, or a long tap is used.
/// If you want an editable text view, where you can short cap an attachment, you have a problem.
/// This gesture recognizer can be added to the text view, and will add requirments in order to recognize before any built-in recognizers.
class AttachmentTapGestureRecognizer: UIGestureRecognizer {
/// Character index of the attachment just tapped
private(set) var attachmentCharacterIndex: Int?
/// The attachment just tapped
private(set) var attachment: NSTextAttachment?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
attachmentCharacterIndex = nil
attachment = nil
let textView = view as! UITextView
if touches.count == 1, let touch = touches.first, touch.tapCount == 1 {
let point = touch.location(in: textView)
let glyphIndex: Int? = textView.layoutManager.glyphIndex(for: point, in: textView.textContainer, fractionOfDistanceThroughGlyph: nil)
let index: Int? = textView.layoutManager.characterIndexForGlyph(at: glyphIndex ?? 0)
if let characterIndex = index, characterIndex < textView.textStorage.length {
if NSAttachmentCharacter == (textView.textStorage.string as NSString).character(at: characterIndex) {
attachmentCharacterIndex = characterIndex
attachment = textView.textStorage.attribute(.attachment, at: characterIndex, effectiveRange: nil) as? NSTextAttachment
state = .recognized
} else {
state = .failed
}
}
} else {
state = .failed
}
}
}
extension UITextView {
/// Add an attachment recognizer to a UITTextView
func add(_ attachmentRecognizer: AttachmentTapGestureRecognizer) {
for other in gestureRecognizers ?? [] {
other.require(toFail: attachmentRecognizer)
}
addGestureRecognizer(attachmentRecognizer)
}
}
This same approach could presumably be used for taps on links.
The delegate method does work but ONLY if the attachment has an image in the image attribute AND if editable = NO! So if you have an image pasted in to the attributedString from somewhere else it seems the data ends up being stored in the fileWrapper and next time you put the attributedString back into the textView the image attribute is nil and the layout manager or whatever gets the image from the fileWrapper.
Somewhere in the documents it does mention that there are no methods in NSTextAttachment for persistence of the image attribute.
To test this try copy a photo from the Photo app and paste it into your textView, now if you hold down your finger on it you should see the default menu pop up. Now if you save this rich text, say into a Core Data entity and then retrieve it the image attribute will be nil but the image data will be in attachment.fileWrapper.regularFileContents
Its a pain, and I would love to know the engineers intention. So you have two options it seems.
Create your own custom NSTextAttachment and include methods for archiving the image and other settings (PLEASE SHOW ME HOW TOO WHEN YOU FIGURE THIS ONE OUT)
Every time prior to putting your string back into textView you find all the attachments and recreated the image attribute like so:
attachment.image = [UIImage imageWithData:attachment.fileWrapper.regularFileContents];
Bear in mind the side effect of doing this is invalidating the fileWrapper. I want to resize the image but also keep the original so I don't loose the full resolution. I think the only way of doing this might be to subclass NSTextAttachment.
EDIT:
I figured out how to create the custom NSTextAttachments - here is a link for those interested http://ossh.com.au/design-and-technology/software-development/implementing-rich-text-with-images-on-os-x-and-ios/
EDIT 2: To customise the menu when in Edit Mode see the following Apple documents, the issue is 'touchEnded' never seems to get called so you might have to try using touchesBegan. Careful you don't interfere with the default editing behaviour though.
https://developer.apple.com/library/ios/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/AddingCustomEditMenuItems/AddingCustomEditMenuItems.html
Note that in the code below you would need to add code after // selection management comment to determine which character was touched, check if it is the special text attachment character and
then modify the edit menu or take some other action.
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *theTouch = [touches anyObject];
if ([theTouch tapCount] == 2 && [self becomeFirstResponder]) {
// selection management code goes here...
// bring up edit menu.
UIMenuController *theMenu = [UIMenuController sharedMenuController];
CGRect selectionRect = CGRectMake (currentSelection.x, currentSelection.y, SIDE, SIDE);
[theMenu setTargetRect:selectionRect inView:self];
[theMenu setMenuVisible:YES animated:YES];
}
}
Alternately you could add a custom menu by adding the menu item and then modifying the canPerformAction method.
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
LOG(#"canPerformAction: called");
if (action == #selector(viewImage)) {
// Check the selected character is the special text attachment character
return YES;
}
return NO;
}
Here is some addition code but its a bit fussy. Second method just disables the default edit menu if an attachment is detected.
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
FLOG(#"touchesBegan:withEvent: called");
if (self.selectedRange.location != NSNotFound) {
FLOG(#" selected location is %d", self.selectedRange.location);
int ch;
if (self.selectedRange.location >= self.textStorage.length) {
// Get the character at the location
ch = [[[self textStorage] string] characterAtIndex:self.selectedRange.location-1];
} else {
// Get the character at the location
ch = [[[self textStorage] string] characterAtIndex:self.selectedRange.location];
}
if (ch == NSAttachmentCharacter) {
FLOG(#" selected character is %d, a TextAttachment", ch);
} else {
FLOG(#" selected character is %d", ch);
}
}
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
FLOG(#"canPerformAction: called");
FLOG(#" selected location is %d", self.selectedRange.location);
FLOG(#" TextAttachment character is %d", NSAttachmentCharacter);
if (self.selectedRange.location != NSNotFound) {
int ch;
if (self.selectedRange.location >= self.textStorage.length) {
// Get the character at the location
ch = [[[self textStorage] string] characterAtIndex:self.selectedRange.location-1];
} else {
// Get the character at the location
ch = [[[self textStorage] string] characterAtIndex:self.selectedRange.location];
}
if (ch == NSAttachmentCharacter) {
FLOG(#" selected character is %d, a TextAttachment", ch);
return NO;
} else {
FLOG(#" selected character is %d", ch);
}
// Check for an attachment
NSTextAttachment *attachment = [[self textStorage] attribute:NSAttachmentAttributeName atIndex:self.selectedRange.location effectiveRange:NULL];
if (attachment) {
FLOG(#" attachment attribute retrieved at location %d", self.selectedRange.location);
return NO;
}
else
FLOG(#" no attachment at location %d", self.selectedRange.location);
}
return [super canPerformAction:action withSender:sender];
}
Swift 3 answer:
func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange) -> Bool {
return true
}
Make sure your textView isEditable = false, isSelectable = true, and isUserInteractionEnabled = true. Duncan's answer did not mention isUserInteractionEnabled, this has to be true, otherwise it wont work.
You can do this programmatically (textView.isEditable = false), or via attributes inspector:
I have altered Drew's gesture recognizer here to subclass UITapGestureRecognizer rather than UIGestureRecognizer.
This offers one advantage in that it only detects discrete taps as opposed to the beginning of a scroll.
import UIKit
import UIKit.UIGestureRecognizerSubclass
// Modified from: https://stackoverflow.com/a/49153247/658604
/// Recognizes a tap on an attachment, on a UITextView.
/// The UITextView normally only informs its delegate of a tap on an attachment if the text view is not editable, or a long tap is used.
/// If you want an editable text view, where you can short cap an attachment, you have a problem.
/// This gesture recognizer can be added to the text view, and will add requirments in order to recognize before any built-in recognizers.
class AttachmentTapGestureRecognizer: UITapGestureRecognizer {
typealias TappedAttachment = (attachment: NSTextAttachment, characterIndex: Int)
private(set) var tappedState: TappedAttachment?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
tappedState = nil
guard let textView = view as? UITextView else {
state = .failed
return
}
if let touch = touches.first {
tappedState = evaluateTouch(touch, on: textView)
}
if tappedState != nil {
// UITapGestureRecognizer can accurately differentiate discrete taps from scrolling
// Therefore, let the super view evaluate the correct state.
super.touchesBegan(touches, with: event)
} else {
// User didn't initiate a touch (tap or otherwise) on an attachment.
// Force the gesture to fail.
state = .failed
}
}
/// Tests to see if the user has tapped on a text attachment in the target text view.
private func evaluateTouch(_ touch: UITouch, on textView: UITextView) -> TappedAttachment? {
let point = touch.location(in: textView)
let glyphIndex: Int? = textView.layoutManager.glyphIndex(for: point, in: textView.textContainer, fractionOfDistanceThroughGlyph: nil)
let index: Int? = textView.layoutManager.characterIndexForGlyph(at: glyphIndex ?? 0)
guard let characterIndex = index, characterIndex < textView.textStorage.length else {
return nil
}
guard NSTextAttachment.character == (textView.textStorage.string as NSString).character(at: characterIndex) else {
return nil
}
guard let attachment = textView.textStorage.attribute(.attachment, at: characterIndex, effectiveRange: nil) as? NSTextAttachment else {
return nil
}
return (attachment, characterIndex)
}
}
SWIFT 4.2
#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 characterIndex < myTextView.textStorage.length {
let attributeValue = myTextView.attributedText.attribute(NSAttributedString.Key.attachment, at: characterIndex, effectiveRange: nil) as? NSTextAttachment
if let _ = attributeValue {
print("TAPPED ATTACHMENT")
}
}
}
Use hitTest to get the touch in a subclassed UITextView. This avoids the issue of messing up the standard editing functions. From the position get the character index and then check the character for the attachment.
I found the common way to solve this kind of clicking attachment in attributed string is using UITextView . But the UITextView is so complicated to disable all the menu, the zoom and so on . So I make a simple version of UILabel wrapper to support attachment clicking . It's simple and easy to use&change . the demo is here:
self.attachmentLabel.attributeText = attributeText;
self.attachmentLabel.numberOflines = 2;
self.attachmentLabel.selectBlock = ^(NSInteger attachmentIndex) {
NSLog(#"attachment:%ld called",attachmentIndex);
};
https://github.com/ximmyxiao/TestAttributeStringClick
Hope it will helps:)

How to detect tap on UITextField?

I have a UITextField that has User Interaction Disabled. So if you tap on this text field, nothing happens. Normally to check if a text field was tapped Id try the delegate methods, but I cannot because user interaction is disabled. Is there any way I can check if the text field was tapped/touched? I change another element to hidden = no; when it is tapped so I was wondering if its even possible enabling user interaction.
Best option is to turn on User Interaction and disable edit action using delegate method.
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField
{
return NO;
}
You can call your method inside that function to detect tap.
Maybe, you can add UITapGestureRecognizer in the superview, detect if the touch is inside the frame, and then do something.
Detect touch if it is inside the frame of the super view
Create UITapGestureRecognizer and add that to the UITextField's super view.
Implement the target selector and check if the gesture's state has ended.
Call your method.
Objective-C
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(didRecognizeTapGesture:)];
[self.textField.superview addGestureRecognizer:tapGesture];
- (void) didRecognizeTapGesture:(UITapGestureRecognizer*) gesture {
CGPoint point = [gesture locationInView:gesture.view];
if (gesture.state == UIGestureRecognizerStateEnded) {
if (CGRectContainsPoint(self.textField.frame, point)) {
[self doSomething];
}
}
}
Swift 3
func viewDidLoad() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didRecognizeTapGesture(_:)))
textField.superView?.addGestureRecognizer(tapGesture)
}
private dynamic func didRecognizeTapGesture(_ gesture: UITapGestureRecognizer) {
let point = gesture.location(in: gesture.view)
guard gesture.state == .ended, textField.frame.contains(point) else { return }
//doSomething()
}

How to disable UITextField editing but still accept touch?

I'm making a UITextField that has a UIPickerView as inputView. Its all good, except that I can edit by copy, paste, cut and select text, and I don't want it. Only the Picker should modify text field.
I've learned that I can disable editing by setting setEnabled or setUserInteractionEnabled to NO. Ok, but the TextField stop responding to touching and the picker don't show up.
What can I do to achieve it?
Using the textfield delegate, there's a method
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
Return NO from this, and any attempt by the user to edit the text will be rejected.
That way you can leave the field enabled but still prevent people pasting text into it.
Translate the answer of Nick to swift:
P/S: Return false => the textfields cannot input, edit by the keyboard. It just can set text by code.EX: textField.text = "My String Here"
override func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
return false
}
This would be the simplest of all:
in viewDidLoad:(set the delegate only for textfields which should not be editable.
self.textfield.delegate=self;
and insert this delegate function:
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField{
return NO;
}
Thats it!
In swift 3+ :
class MyViewController: UIViewController, UITextFieldDelegate {
override func viewDidLoad() {
self.myTextField.delegate = self
}
func textFieldShouldBeginEditing(textField: UITextField) -> Bool {
if textField == myTextField {
// code which you want to execute when the user touch myTextField
}
return false
}
}
Simply place a UIButton exactly over the entire UITextField with no Label-text which makes it "invisible". This button can receive and delegate touches instead of the Textfield and the content of the TextField is still visible.
It would be more elegant to create a custom subclass of UITextField that returns NO for all calls to canPerformAction:withSender: (or at least where action is #selector(cut) or #selector(paste)), as described here.
In addition, I'd also implement
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
as per Nick's suggestion in order to disable inputting text from Bluetooth keyboards.
In Swift:
func textFieldShouldBeginEditing(textField: UITextField) -> Bool {
questionField.resignFirstResponder();
// Additional code here
return false
}
I used the solution provided by MrMage. The only thing I'd add is you should resign the UITextView as first responder, otherwise you're stuck with the text selected.
Here's my swift code:
class TouchableTextView : UITextView {
override func canPerformAction(action: Selector, withSender sender: AnyObject?) -> Bool {
self.resignFirstResponder()
return false
}
override func shouldChangeTextInRange(range: UITextRange, replacementText text: String) -> Bool {
self.resignFirstResponder()
return false
}
}
To prevent editing of UITextField while using UIPickerView for selecting values(in Swift):
self.txtTransDate = self.makeTextField(self.transDate, placeHolder: "Specify Date")
self.txtTransDate?.addTarget(self, action: "txtTransDateEditing:", forControlEvents: UIControlEvents.EditingDidBegin)
func makeTextField(text: String?, placeHolder: String) -> UITextField {
var textField = UITextField(frame: CGRect(x: 140, y: 0, width: 220.00, height: 40.00));
textField.placeholder = placeHolder
textField.text = text
textField.borderStyle = UITextBorderStyle.Line
textField.secureTextEntry = false;
textField.delegate = self
return textField
}
func txtTransDateEditing(sender: UITextField) {
var datePickerView:UIDatePicker = UIDatePicker()
datePickerView.datePickerMode = UIDatePickerMode.Date
sender.inputView = datePickerView
datePickerView.addTarget(self, action: Selector("datePickerValueChanged:"), forControlEvents: UIControlEvents.ValueChanged)
}
func datePickerValueChanged(sender: UIDatePicker) {
var dateformatter = NSDateFormatter()
dateformatter.dateStyle = NSDateFormatterStyle.MediumStyle
self.txtTransDate!.text = dateformatter.stringFromDate(sender.date)
}
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
self.resignFirstResponder()
return false
}
For an alternative that handles the UIPickerView and Action Sheets, checkout ActionSheetPicker
https://github.com/TimCinel/ActionSheetPicker
It's cocoapods enabled. It handles all of the cancel and done buttons on the Action Sheet. The examples within the sample project are great. I choose the ActionSheetStringPicker, which handles easily just String based options, but the API can handle most anything that I can think of.
I originally started a solution much like the checkmarked answer, but stumbled onto this project and took me roughly 20 minutes to get things integrated into my app for usage including using cocopods: ActionSheetPicker (~> 0.0)
Hope this helps.
Download the git project and look at the following classes:
ActionSheetPickerViewController.m
ActionSheetPickerCustomPickerDelegate.h
Here is roughly most of the code that I added, plus the *.h imports.
- (IBAction)gymTouched:(id)sender {
NSLog(#"gym touched");
[ActionSheetStringPicker showPickerWithTitle:#"Select a Gym" rows:self.locations initialSelection:self.selectedIndex target:self successAction:#selector(gymWasSelected:element:) cancelAction:#selector(actionPickerCancelled:) origin:sender];
}
- (void)actionPickerCancelled:(id)sender {
NSLog(#"Delegate has been informed that ActionSheetPicker was cancelled");
}
- (void)gymWasSelected:(NSNumber *)selectedIndex element:(id)element {
self.selectedIndex = [selectedIndex intValue];
//may have originated from textField or barButtonItem, use an IBOutlet instead of element
self.txtGym.text = [self.locations objectAtIndex:self.selectedIndex];
}
-(BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
return NO; // Hide both keyboard and blinking cursor.
}
if you are ready to create your custom textfield then you can just use this answer from another stackoverflow question.
https://stackoverflow.com/a/42698689/9369035
just override those three method as in above answer and that is enough. at least so far as I tested.
This workaround works. Put a transparent UIView above the text field and implement the following code:
- (void)viewDidLoad
{
[super viewDidLoad];
UILongPressGestureRecognizer *press = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(longPress)];
[transparentView addGestureRecognizer:press];
[press release];
press = nil;
}
-(void)longPress
{
txtField.userInteractionEnabled = NO;
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
txtField.userInteractionEnabled = YES;
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[txtField becomeFirstResponder];
}
Make your inputView be presented by an hidden textfield which also change the text of the presented and disabled one.
Not completely sure about how hacky this is but the only thing that did the trick in my case was adding a target action and calling endEditing. Since my picker controls my UITextField.text value, I could dismiss the keyboard as soon as the user clicks on the field. Here's some code:
uiTextFieldVariable.addTarget(self, action: #selector(dismissKeyboard), for: .editingDidBegin)
#objc private func dismissKeyboard() {
endEditing(true)
}
If you are using IQKeyboardManagerSwift pod then use textField.enableMode = .disabled
else If you are using RxSwift & RxCocoa pods then
textField.rx.controlEvent(.editingDidBegin)
.subscribe(onNext: {
[weak self] _ in let _ = self?.textField.endEditing(true)
}).disposed(by: bag)
else use delegate method of textFiled
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
return false
}
I used :
[self.textField setEnabled:NO];
and its work fine
This worked for me
[textview setEditable:NO];
The above answers are overcomplicating the situation.

Resources