UITextView Delayed AttributedString Link SwiftUI - ios

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.

Related

Swift UITextView Delegate

I am having a problem and have searched all across StackO and did not see a solution.
I have a UITextview extension with TextViewDelegate that I call inside of my VC so that i can have a placeholder label. The problem is i now need to add a func that checks for remaining chars in that same textView which i am able to get to work properly. But i cant grab a label to present it on the VC from that extension. I have been trying delegates but since it is a delegate itself i cant use my normal methods. What is the best route to go about this? Thank You for your help!
Here is the code. The placeholder label code is left out since it will make everything longer and I do not feel its needed for a solution. But I can add if necessary. And i can not move this code straight into VC as i need this extension to stay like this.
extension UITextView: UITextViewDelegate {
/// When the UITextView change, show or hide the label based on if the UITextView is empty or not
public func textViewDidChange(_ textView: UITextView) {
if let placeholderLabel = self.viewWithTag(100) as? UILabel {
placeholderLabel.isHidden = !self.text.isEmpty
}
checkRemainingChars(textView: textView)
}
func checkRemainingChars(textView: UITextView) {
let allowedChars = 140
if let charsInTextField = textView.text?.count {
let charsInLabel = charsInTextField
let remainingChars = charsInLabel
if remainingChars <= allowedChars {
//Need to grab this label
charsLeftLabel.textColor = UIColor.lightGray
}
if remainingChars >= 120 {
//Need to grab this label
charsLeftLabel.textColor = UIColor.orange
}
if remainingChars >= allowedChars {
//Need to grab this label
charsLeftLabel.textColor = UIColor.red
}
//This prints fine
print("Remaining chars is \(remainingChars)/140")
//Need to grab this label
charsLeftLabel.text = String(remainingChars)
}
}
Thanks again.

SwiftUI Custom TextField with UIViewRepresentable Issue with ObservableObject and pushed View

I created a UIViewRepresentable to wrap UITextField for SwiftUI, so I can e.g. change the first responder when the enter key was tapped by the user.
This is my UIViewRepresentable (I removed the first responder code to keep it simple)
struct CustomUIKitTextField: UIViewRepresentable {
#Binding var text: String
var placeholder: String
func makeUIView(context: UIViewRepresentableContext<CustomUIKitTextField>) -> UITextField {
let textField = UITextField(frame: .zero)
textField.delegate = context.coordinator
textField.placeholder = placeholder
return textField
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomUIKitTextField>) {
uiView.text = text
uiView.setContentHuggingPriority(.defaultHigh, for: .vertical)
uiView.setContentCompressionResistancePriority(.required, for: .vertical)
}
func makeCoordinator() -> CustomUIKitTextField.Coordinator {
Coordinator(parent: self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: CustomUIKitTextField
init(parent: CustomUIKitTextField) {
self.parent = parent
}
func textFieldDidChangeSelection(_ textField: UITextField) {
parent.text = textField.text ?? ""
}
}
}
The first screen of the app has a "Sign in with email" button which pushes MailView that displays a CustomUIKitTextField and uses a #Published property of an ObservableObject view model as the TextField's text.
struct MailView: View {
#ObservedObject var viewModel: MailSignUpViewModel
var body: some View {
VStack {
CustomUIKitTextField(placeholder: self.viewModel.placeholder,
text: self.$viewModel.mailAddress)
.padding(.top, 30)
.padding(.bottom, 10)
NavigationLink(destination: UsernameView(viewModel: UsernameSignUpViewModel())) {
Text("Next")
}
Spacer()
}
}
}
Everything works fine until I push another view like MailView, say e.g. UsernameView. It is implemented exactly in the same way, but somehow the CustomUIKitTextField gets an updateUIView call with an empty string once I finish typing.
There is additional weird behavior like when I wrap MailView and UsernameView in another NavigationView, everything works fine. But that is obviously not the way to fix it, since I would have multiple NavigationViews then.
It also works when using an #State property instead of a #Published property inside a view model. But I do not want to use #State since I really want to keep the model code outside the view.
Is there anybody who faced the same issue or a similar one?
It looks like you’re using the wrong delegate method. textFieldDidChangeSelection will produce some inconsistent results (which is what it sounds like you’re dealing with). Instead, I recommend using textFieldDidEndEditing which will also give you access to the passed in control, but it guarantees that you’re getting the object as it is resigning the first responder. This is important because it means you’re getting the object after the properties have been changed and it’s releasing the responder object.
So, I would change your delegate method as follows:
func textFieldDidEndEditing(_ textField: UITextField) {
parent.text = textField.text ?? ""
}
For more info, see this link for the textFieldDidEndEditing method:
https://developer.apple.com/documentation/uikit/uitextfielddelegate/1619591-textfielddidendediting
And this link for info on the UITextFieldDelegate object:
https://developer.apple.com/documentation/uikit/uitextfielddelegate
EDIT
Based on the comment, if you're looking to examine the text everytime it changes by one character, you should implement this delegate function:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// User pressed the delete key
if string.isEmpty {
// If you want to update your query string here after the delete key is pressed, you can rebuild it like we are below
return true
}
//this will build the full string the user intends so we can use it to build our search
let currentText = textField.text ?? ""
let replacementText = (currentText as NSString).replacingCharacters(in: range, with: string)
// You can set your parent.text here if you like, or you can fire the search function as well
// When you're done though, return true to indicate that the textField should display the changes from the user
return true
}
I also needed a UITextField representation in SwiftUI, which reacts to every character change, so I went with the answer by binaryPilot84. While the UITextFieldDelegate method textField(_:shouldChangeCharactersIn:replacementString:) is great, it has one caveat -- every time we update the text with this method, the cursor moves to the end of the text. It might be desired behavior. However, it was not for me, so I implemented target-action like so:
public func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
textField.addTarget(context.coordinator, action: #selector(context.coordinator.textChanged), for: .editingChanged)
return textField
}
public final class Coordinator: NSObject {
#Binding private var text: String
public init(text: Binding<String>) {
self._text = text
}
#objc func textChanged(_ sender: UITextField) {
guard let text = sender.text else { return }
self.text = text
}
}

How to embed a custom button within a label in swift?

I've created a custom info button that I want to put inside a regular UILabel.
The idea is to have the screen say "Tap the (BUTTON HERE) for more information". Is there a way to do this without creating two UILabels? And if creating 2 labels is the only way, how can I put everything on one line?
I tried to do (button) inside the label.text but that shows the button's properties instead of placing the button. I also tried label.addSubview(button) which works but adds the button in the wrong place.
The best way to do this is using a UITextView with an NSAttributedString, where one of the attributes is your link.
let textView = UITextView()
textView.delegate = self
// These allow the link to be tapped
textView.isEditable = false
textView.isScrollEnabled = false
// Removes padding that UITextView uses, making it look more like a UILabel
textView.textContainer.lineFragmentPadding = 0.0
textView.textContainerInset = .zero
Then for the NSAttributedString
let text = "Tap HERE for more information"
let linkText = "HERE"
// Get range for tappable section
let linkRange = (text as NSString).range(of: linkText)
// Styling
let attributes: [NSAttributedString.Key: Any] = [
.foregroundColor: UIColor.black
]
// Actual Link!
let linkTextAttributes: [NSAttributedString.Key: Any] = [
.underlineStyle: NSUnderlineStyle.single.rawValue,
.link: "https://www.example.com" //The link you want to link to
]
let attributedString = NSMutableAttributedString(string: text, attributes: attributes)
attributedString.addAttributes(linkTextAttributes, range: linkRange)
Then use these UITextView delegate functions
// Removes a lot of the actions when user selects text
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return false
}
// Handle the user tapping the link however you like here
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
viewModel.urlTapped(URL)
return false
}

UITextView URL interaction handle

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.

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.

Resources