UITextView URL interaction handle - ios

I have a UITextView with selectable = true, isUserInteractionEnabled = true, dataDetectorTypes = .all
and i use UITextViewDelegate:
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
for recognizer in textView.gestureRecognizers! {
if recognizer.isKind(of: UITapGestureRecognizer.self) {
if recognizer.state == .ended {
return true
}
}
}
longTouchCell()
return false
}
If a single tap is done to the URL, then I want to open this URL by returning true, else I would like to do custom action longTouchCell().
However, sometimes a single tap also triggers custom action longTouchCell(). Namely, if after a custom action, I wait for some time duration long enough and do a single tap again, then it works as expected, but a quick tap right after a long press will trigger as if the case of a long press.

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.

How to detect all links in a text and handle tap on them [duplicate]

Is it possible to perform custom action when user touch autodetected phone link in UITextView. Please do not advice to use UIWebView instead.
And please don't just repeat text from apple classes reference
- certainly I've already read it.
Thanks.
Update: From ios10,
- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange interaction:(UITextItemInteraction)interaction;
From ios7 and Later UITextView has the delegate method:
- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange *NS_DEPRECATED_IOS(7_0, 10_0, "Use textView:shouldInteractWithURL:inRange:forInteractionType: instead");*
to intercept the clicks to links. And this is the best way to do it.
For ios6 and earlier a nice way to do this is to by subclassing UIApplication and overwriting the -(BOOL)openURL:(NSURL *)url
#interface MyApplication : UIApplication {
}
#end
#implementation MyApplication
-(BOOL)openURL:(NSURL *)url{
if ([self.delegate openURL:url])
return YES;
else
return [super openURL:url];
}
#end
You will need to implement openURL: in your delegate.
Now, to have the application start with your new subclass of UIApplication, locate the file main.m in your project. In this small file that bootstraps your app, there is usually this line:
int retVal = UIApplicationMain(argc, argv, nil, nil);
The third parameter is the class name for your application. So, replacing this line for:
int retVal = UIApplicationMain(argc, argv, #"MyApplication", nil);
This did the trick for me.
In iOS 7 or Later
You can use the following UITextView delegate Method:
- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange
The text view calls this method if the user taps or long-presses the URL link. Implementation of this method is optional. By default, the text view opens the application responsible for handling the URL type and passes it the URL. You can use this method to trigger an alternative action, such as displaying the web content at the URL in a web view within the current application.
Important:
Links in text views are interactive only if the text view is
selectable but noneditable. That is, if the value of the UITextView
the selectable property is YES and the isEditable property is NO.
With Swift 5 and iOS 12, you can use one of the three following patterns in order to interact with links in a UITextView.
#1. Using UITextView's dataDetectorTypes property.
The simplest way to interact with phone numbers, urls or addresses in a UITextView is to use dataDetectorTypes property. The sample code below shows how to implement it. With this code, when the user taps on the phone number, a UIAlertController pops up.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let textView = UITextView()
textView.text = "Phone number: +33687654321"
textView.isUserInteractionEnabled = true
textView.isEditable = false
textView.isSelectable = true
textView.dataDetectorTypes = [.phoneNumber]
textView.isScrollEnabled = false
textView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(textView)
textView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
textView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
textView.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor).isActive = true
}
}
#2. Using UITextViewDelegate's textView(_:shouldInteractWith:in:interaction:) method
If you want to perform some custom action instead of making a UIAlertController pop up when you tap on a phone number while using dataDetectorTypes, you have to make your UIViewController conform to UITextViewDelegate protocol and implement textView(_:shouldInteractWith:in:interaction:). The code below shows how to implement it:
import UIKit
class ViewController: UIViewController, UITextViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let textView = UITextView()
textView.delegate = self
textView.text = "Phone number: +33687654321"
textView.isUserInteractionEnabled = true
textView.isEditable = false
textView.isSelectable = true
textView.dataDetectorTypes = [.phoneNumber]
textView.isScrollEnabled = false
textView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(textView)
textView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
textView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
textView.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor).isActive = true
}
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
/* perform your own custom actions here */
print(URL) // prints: "tel:+33687654321"
return false // return true if you also want UIAlertController to pop up
}
}
#3. Using NSAttributedString and NSAttributedString.Key.link
As an alternative, you can use NSAttributedString and set a URL for its NSAttributedString.Key.link attribute.The sample code below shows a possible implementation of it. With this code, when user taps on the attributed string, a UIAlertController pops up.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let attributedString = NSMutableAttributedString(string: "Contact: ")
let phoneUrl = NSURL(string: "tel:+33687654321")! // "telprompt://+33687654321" also works
let attributes = [NSAttributedString.Key.link: phoneUrl]
let phoneAttributedString = NSAttributedString(string: "phone number", attributes: attributes)
attributedString.append(phoneAttributedString)
let textView = UITextView()
textView.attributedText = attributedString
textView.isUserInteractionEnabled = true
textView.isEditable = false
textView.isSelectable = true
textView.isScrollEnabled = false
textView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(textView)
textView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
textView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
textView.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor).isActive = true
}
}
For Swift 3
textView.delegate = self
extension MyTextView: UITextViewDelegate {
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
GCITracking.sharedInstance.track(externalLink: URL)
return true
}
}
or if target is >= IOS 10
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool
Swift version:
Your standard UITextView setup should look something like this, don't forget the delegate and dataDetectorTypes.
var textView = UITextView(x: 10, y: 10, width: CardWidth - 20, height: placeholderHeight) //This is my custom initializer
textView.text = "dsfadsaf www.google.com"
textView.selectable = true
textView.dataDetectorTypes = UIDataDetectorTypes.Link
textView.delegate = self
addSubview(textView)
After your class ends add this piece:
class myVC: UIViewController {
//viewdidload and other stuff here
}
extension MainCard: UITextViewDelegate {
func textView(textView: UITextView, shouldInteractWithURL URL: NSURL, inRange characterRange: NSRange) -> Bool {
//Do your stuff over here
var webViewController = SVModalWebViewController(URL: URL)
view.presentViewController(webViewController, animated: true, completion: nil)
return false
}
}
Swift 4:
1) Create the following class (subclassed UITextView):
import Foundation
protocol QuickDetectLinkTextViewDelegate: class {
func tappedLink()
}
class QuickDetectLinkTextView: UITextView {
var linkDetectDelegate: QuickDetectLinkTextViewDelegate?
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let glyphIndex: Int? = layoutManager.glyphIndex(for: point, in: textContainer, fractionOfDistanceThroughGlyph: nil)
let index: Int? = layoutManager.characterIndexForGlyph(at: glyphIndex ?? 0)
if let characterIndex = index {
if characterIndex < textStorage.length {
if textStorage.attribute(NSLinkAttributeName, at: characterIndex, effectiveRange: nil) != nil {
linkDetectDelegate?.tappedLink()
return self
}
}
}
return nil
}
}
2) Wherever you set up your textview, do this:
//init, viewDidLoad, etc
textView.linkDetectDelegate = self
//outlet
#IBOutlet weak var textView: QuickDetectLinkTextView!
//change ClassName to your class
extension ClassName: QuickDetectLinkTextViewDelegate {
func tappedLink() {
print("Tapped link, do something")
}
}
If you're using storyboard, make sure your textview looks like this in the right pane identity inspector:
Voila! Now you get the link tap immediately instead of when the URL shouldInteractWith URL method
application:handleOpenURL: is called when another app opens your app by opening a URL with a scheme your app supports. It's not called when your app begins opening a URL.
I think the only way to do what Vladimir wants is to use a UIWebView instead of a UITextView. Make your view controller implement UIWebViewDelegate, set the UIWebView's delegate to the view controller, and in the view controller implement webView:shouldStartLoadWithRequest:navigationType: to open [request URL] in a view instead of quitting your app and opening it in Mobile Safari.
I haven't tried that myself but you can try to implement application:handleOpenURL: method in your application delegate - it looks like all openURL request pass through this callback.
Not sure how you would intercept the detected data link, or what type of function you need to run. But you may be able to utilize the didBeginEditing TextField method to run a test/scan through the textfield if you know what your looking for..such as comparing text strings that meet ###-###-#### format, or begin with "www." to grab those fields, but you would need to write a little code to sniff through the textfields string, reconize what you need, and then extract it for your function's use. I don't think this would be that difficult, once you narrowed down exactly what it is that you wanted and then focussed your if() statement filters down to very specific matching pattern of what you needed.
Of couse this implies that the user is going to touch the textbox in order to activate the didBeginEditing(). If that is not the type of user interaction you were looking for you could just use a trigger Timer, that starts on ViewDidAppear() or other based on need and runs through the textfields string, then at the end of you run through the textfield string methods that you built, you just turn the Timer back off.

In Swift, how do I detect a double tap from the spacebar in UITextView?

I'm working on an app that is heavily dependent on a UITextView. The desired behavior is when a user double taps the spacebar, the cursor will indent 5 spaces. How would I do this?
You should use a timer which checks for short time intervals and the shouldChangeTextInRange delegate method for UITextView and write a condition for space strings. After that, you can use the insertText method of UITextInput, one of UITextView's protocols:
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if text == " " {
if isSecondSpace {
(textView as UIKeyInput).insertText(" ") //5 spaces
} else {
isSecondSpace = true
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2) {
self.isSecondSpace = false
}
}
return false
}
return true
}
Use the UIText​View​Delegate and in the text​View(UIText​View, should​Change​Text​In:​ NSRange, replacement​Text:​ String) function you keep track of the space inputs and the times. When you receive two space inputs within a short time frame, you manipulate the textfield with the 5 spaces.

Change 'Return' button function to 'Done' in swift in UITextView

I would like to get rid of the "return" function of the keyboard while the user is typing, so there are no new lines, so instead I would like the 'return' key to function as 'Done' so it would hide the keyboard.
I am using a UITextView, that is editable, so the user is able to type their post, and post it to the main timeline, but since I have fixed cells, I don't want the user to be able to press 'return' and their post would be out of range of the timeline.
I found this that works with UITextField, but not with UITextView:
func textFieldShouldReturn(textField: UITextField!) -> Bool {
textField.resignFirstResponder() //if desired
return true
}
So I just wanted to know if there is a way to do that in a UITextView, or at least to be able to hide the keyboard if pressed return, instead of creating a new line.
You can set the return key type of the text field:
textField.returnKeyType = UIReturnKeyType.done
Update
You can definitely use the same approach to set the return key to "Done", as mentioned above. However, UITextView doesn't provide a callback when user hits the return key. As a workaround, you can try to handle the textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) delegate call, and dismiss the keyboard when you detect the input of a new line character:
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if (text == "\n") {
textView.resignFirstResponder()
}
return true
}
I have tried many codes and finally this worked for me in Swift 3.0 Latest [April 2019] this achieved using UITextFields
The "ViewController" class should be inherited the "UITextFieldDelegate" for making this code working.
class ViewController: UIViewController,UITextFieldDelegate
Add the Text field with the Proper Tag number and this tag number is used to take the control to appropriate text field based on incremental tag number assigned to it.
override func viewDidLoad() {
userNameTextField.delegate = self
userNameTextField.tag = 0
userNameTextField.returnKeyType = .next
passwordTextField.delegate = self
passwordTextField.tag = 1
passwordTextField.returnKeyType = .go
}
In the above code, the "returnKeyType = UIReturnKeyType.next" where will make the Key pad return key to display as "Next" you also have other options as "Join/Go" etc, based on your application change the values.
This "textFieldShouldReturn" is a method of UITextFieldDelegate controlled and here we have next field selection based on the Tag value incrementation.
func textFieldShouldReturn(_ textField: UITextField) -> Bool
{
if let nextField = textField.superview?.viewWithTag(textField.tag + 1) as? UITextField {
nextField.becomeFirstResponder()
} else {
textField.resignFirstResponder()
return true;
}
return false
}
If you're working with a storyboard or xib, you can change the UITextView's Return button to 'Done' (or various other options) within Interface Builder, without the need for any setup code. Just look for this option in the Attributes inspector:
From there, you just pair it up with the UITextViewDelegate code that others have already provided here.
Swift v5:
extension ExampleViewController: UITextViewDelegate {
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if (text == "\n") {
textView.resignFirstResponder()
}
return true
}
}
And then, in your viewDidLoad() method:
exampleTextView.delegate = self
Working in Swift 4
Add this in viewDidLoad().
textField.returnKeyType = UIReturnKeyType.Done
Add this anywhere you like.
extension UITextView: UITextViewDelegate {
public func textViewDidChange(_ textView: UITextView) {
if text.last == "\n" { //Check if last char is newline
text.removeLast() //Remove newline
textView.resignFirstResponder() //Dismiss keyboard
}
}
}

Hyperlink / URL inside textView paragraph

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;

Resources