I have a pop-up window, that shows terms of service & privacy policy for users, how to disable text selection but keep links tappable. screenshot
If I set:
SOLVED: By adding textViewDidChangeSelection method in UIViewDelegate and setting
textView.isSelectable = false
textView.isUserInteractionEnabled = true
textView.isSelectable = true
Links become uninteractable.
How do I keep the link tappable and text not selectable?
My whole class:
// HyperLinkTextView.swift
import SwiftUI
struct Hyperlink {
var word: String
var url: NSURL
}
struct HyperLinkTextView: UIViewRepresentable {
private var text: String
private var links: [Hyperlink]
let textView = UITextView()
init(text: String, links: [Hyperlink]) {
self.text = text
self.links = links
}
func makeUIView(context: Self.Context) -> UITextView {
let attributedString = NSMutableAttributedString(string: text)
links.forEach { hyperlink in
let linkAttributes = [NSAttributedString.Key.link: hyperlink.url]
var nsRange = NSMakeRange(0, 0)
if let range = text.range(of: hyperlink.word) {
nsRange = NSRange(range, in: text)
}
attributedString.setAttributes(linkAttributes, range: nsRange)
attributedString.addAttribute(NSAttributedString.Key.underlineStyle, value: NSNumber(value: 1), range: nsRange)
}
textView.isEditable = false
textView.delegate = context.coordinator
textView.attributedText = attributedString
textView.linkTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor(Theme.colorClickables)]
textView.isUserInteractionEnabled = true
textView.isSelectable = true
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.font = UIFont(name: "ArialMT", size: 18)
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
public class Coordinator: NSObject, UITextViewDelegate, NSLayoutManagerDelegate {
weak var textView: UITextView?
public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction, replacementText text: String) -> Bool {
return true
}
func textViewDidChangeSelection(_ textView: UITextView) {
if textView.selectedTextRange != nil {
textView.delegate = nil
textView.selectedTextRange = nil
textView.delegate = self
}
}
}
}
Add this UITextViewDelegate textViewDidChangeSelection and comment out isEditable and isSelectable:
func textViewDidChangeSelection(_ textView: UITextView) {
if textView.selectedTextRange != nil {
textView.delegate = nil
textView.selectedTextRange = nil
textView.delegate = self
}
}
Related
So in my text editor, I'd like to know the position of the cursor geometrically. I'm also planning to append some text after that position.
So how do I do this?
Okay... So I figured out a way to do this.
First, I created a struct to store the cursor positions
import foundation
struct CursorPosition {
start: Int
end: Int
}
Then I initialize it to be static
class Global {
public static var cursorPosition = CursorPosition(start: 0, end: 0)
}
Then finally, I created a custom view to match the SwiftUI TextEditor and listen for selection change and update the CursorPosition
import UIKit
import SwiftUI
fileprivate struct UITextViewWrapper: UIViewRepresentable {
typealias UIViewType = UITextView
#Binding var text: String
var onDone: (() -> Void)?
func makeUIView(context: UIViewRepresentableContext<UITextViewWrapper>) -> UITextView {
let textField = UITextView()
textField.delegate = context.coordinator
textField.isEditable = true
textField.font = UIFont.preferredFont(forTextStyle: .body)
textField.isSelectable = true
textField.isUserInteractionEnabled = true
textField.isScrollEnabled = true
textField.backgroundColor = UIColor.clear
if nil != onDone {
textField.returnKeyType = .done
}
textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return textField
}
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<UITextViewWrapper>) {
if uiView.text != self.text {
uiView.text = self.text
}
if uiView.window != nil, !uiView.isFirstResponder {
uiView.becomeFirstResponder()
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text, onDone: onDone)
}
final class Coordinator: NSObject, UITextViewDelegate {
var text: Binding<String>
var onDone: (() -> Void)?
init(text: Binding<String>, onDone: (() -> Void)? = nil) {
self.text = text
self.onDone = onDone
}
func textViewDidChange(_ uiView: UITextView) {
text.wrappedValue = uiView.text
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if let onDone = self.onDone, text == "\n" {
textView.resignFirstResponder()
onDone()
return false
}
return true
}
func textViewDidChangeSelection(_ textView: UITextView) {
if let range = textView.selectedTextRange {
Global.cursorPosition.start = textView.offset(from: textView.beginningOfDocument, to: range.start)
Global.cursorPosition.end = textView.offset(from: textView.beginningOfDocument, to: range.end)
}
}
}
}
struct EditText: View {
private var placeholder: String
private var onCommit: (() -> Void)?
#Binding private var text: String
private var internalText: Binding<String> {
Binding<String>(get: { self.text } ) {
self.text = $0
self.showingPlaceholder = $0.isEmpty
}
}
#State private var showingPlaceholder = false
init (_ placeholder: String = "", text: Binding<String>, onCommit: (() -> Void)? = nil) {
self.placeholder = placeholder
self.onCommit = onCommit
self._text = text
self._showingPlaceholder = State<Bool>(initialValue: self.text.isEmpty)
}
var body: some View {
UITextViewWrapper(text: self.internalText, onDone: onCommit)
.background(placeholderView, alignment: .topLeading)
}
var placeholderView: some View {
Group {
if showingPlaceholder {
Text(placeholder).foregroundColor(.gray)
.padding(.leading, 4)
.padding(.top, 8)
}
}
}
}
And using it:
EditText("", text: $text)
.onChange(of: text){ _ in
let cursorStart = Global.cursorPosition.start
}
I have a UITextView that has some text, and then should have a clickable string at the end of the paragraph, but I'm not sure how to approach this. I wanted to use attributed strings, but I cannot add a target to the attributed string to make it clickable. How can I accomplish this?
You can use UITextViewDelegate to detect iuser interation.
#IBOutlet weak var mapTextView: UITextView!
func setupTextView() {
let mapAttributedText = NSMutableAttributedString(string: "Click here to get directions")
mapAttributedText.addAttribute(NSAttributedString.Key.link, value: "customValue", range: mapAttributedText.mutableString.range(of: "Click here"))
let linkArr = [NSAttributedString.Key.foregroundColor: UIColor.blue,NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue,NSAttributedString.Key.underlineColor: UIColor.blue] as [NSAttributedString.Key : Any]
mapTextView.linkTextAttributes = linkArr
mapTextView.attributedText = mapAttributedText
mapTextView.delegate = self
mapTextView.isEditable = false
}
extension YourVCName: UITextViewDelegate {
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
if URL.absoluteString == "customValue" {
// Do something
}
return false
}
}
I have this Custom UITextView that uses "ShouldInteractWith" method:
class StudyText: UITextView, UITextViewDelegate {
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
print(URL)
return false
}
}
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
}
}
I have this extension to set a link to the attributed text:
extension NSMutableAttributedString {
func apply(link: String, subString: String) {
if let range = self.string.range(of: subString) {
self.apply(link: link, onRange: NSRange(range, in: self.string))
}
}
private func apply(link: String, onRange: NSRange) {
self.addAttributes([NSAttributedString.Key.link: link], range: onRange)
}
}
And I created this layout:
struct contentView: View {
#State text: NSMutableAttributedString = NSMutableAttributedString(string: "")
var body: some View {
VStack {
ClickableText(text: self.$text)
}
.onAppear{
let myText = "Click Me!"
let attributedString = NSMutableAttributedString(string: myText)
attributedString.apply(link: "Soeme random link", subString: myText)
self.text = attributedString
}
}
}
When I click on the text view it doesn't print anything to the console and sometimes it crashes.
How can I fix this?
It must be provided valid URL, like
let attributedString = NSMutableAttributedString(string: myText)
attributedString.apply(link: "https://www.google.com", subString: myText)
I have a signup page with privacy and term&condition. I am using textview controller for show content when I was clicked any of them show another view controller, I tried this code but it's not working,
let signUpTermsAndPrivacyString = NSMutableAttributedString(string: "I have read and understood PayGyft Terms of Usage and Privacy Policy",attributes: [NSAttributedStringKey.font: UIFont(name: "Helvetica", size: 15.0)!,NSAttributedStringKey(rawValue: NSAttributedStringKey.foregroundColor.rawValue): UIColor.darkGray])
let termsString = signUpTermsAndPrivacyString.mutableString.range(of: "Terms of Usage")
print("termsSTring",termsString)
signUpTermsAndPrivacyString.addAttribute(.link, value: "http://gregoryadunbar.com", range: termsString)
let privacyString = signUpTermsAndPrivacyString.mutableString.range(of: "Privacy Policy")
signUpTermsAndPrivacyString.addAttribute(.link, value:"http://gregoryadunbar.com", range: privacyString)
termsPrivacyPolicyTextView.attributedText = signUpTermsAndPrivacyString
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
if (URL.absoluteString == termsURLString) {
print("termsURLString")
} else if (URL.absoluteString == privacyURLString) {
print("privacyURLString")
}
return false
}
You need to add myTextView.linkTextAttributes
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
let attributes = [NSAttributedStringKey.foregroundColor : UIColor.init(rgb: 0x646464),
NSAttributedStringKey.font : AppFont.getFont(fontType: .regular, ofSize: 16),
NSAttributedStringKey.paragraphStyle : paragraphStyle]
let attributedText = NSMutableAttributedString(string: descriptonText,
attributes: attributes)
if let range = attributedText.string.range(of: "terms & conditions") {
let nsRange = NSRange(range, in: attributedText.string)
attributedText.addAttributes([NSAttributedStringKey.link : "link://T&C"], range: nsRange)
}
let linkColor = UIColor.init(red: 47, green: 117, blue: 83)
let linkAttrs: [String: Any] = [NSAttributedStringKey.foregroundColor.rawValue : linkColor,
NSAttributedStringKey.underlineColor.rawValue : linkColor,
NSAttributedStringKey.underlineStyle.rawValue : NSUnderlineStyle.styleSingle.rawValue]
descriptionTextView.linkTextAttributes = linkAttrs
let textView = UITextView(frame: descriptionTextView.bounds)
textView.attributedText = attributedText
textView.layoutIfNeeded()
textView.sizeToFit()
let size = textView.sizeThatFits(CGSize(width: textView.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
descriptionTextViewHeight.constant = size.height
descriptionTextView.attributedText = attributedText
And in delegate:
func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
return false
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
return false
}
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
debugPrint(URL.absoluteString)
if URL.absoluteString.contains("link://") {
// my func call
dismissAnimated()
}
return false
}
Also set isEditable to false and isSelectable to true. Subclass textView and return false in canPerformAction. In awakeFromNib set textDragInteraction?.isEnabled = false
I have text in a TextView, there are some words in the text, in the case when the user clicks on it, an alert should appear with a word, which for example can mean a word translation into some language.
I tried to use a dictionary, where the key is the word in the text, and the word in the alert is a value. But it does not work. There is an alert with the wrong word.
import UIKit
class ViewController: UIViewController , UITextViewDelegate {
private let kURLString = "https://www.mywebsite.com"
let dictionary = ["website" : "Johny" , "visit" : "Bilbo"]
var keyOne : String?
var valueOne : String?
#IBOutlet weak var text: UITextView! {
didSet{
text.delegate = self
}
}
override func viewDidLoad() {
super.viewDidLoad()
let originalText = "Please visit the website for more information."
let attributedOriginalText = NSMutableAttributedString(string: originalText)
for (key , value) in dictionary {
keyOne = key
valueOne = value
let linkRange = attributedOriginalText.mutableString.range(of: keyOne!)
attributedOriginalText.addAttribute(.link, value: kURLString, range: linkRange)
}
text.attributedText = attributedOriginalText
}
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
if (URL.absoluteString == kURLString) {
alert(value: valueOne!)
}
return false
}
func alert (value : String) {
let alert = UIAlertController (title: nil, message: value, preferredStyle: .alert)
let restartAction = UIAlertAction(title: "Ок", style: .default , handler : { (UIAlertAction) in
self.viewDidLoad()
})
alert.addAction(restartAction)
present(alert, animated: true, completion: nil)
}
}
You can do this by creating an extension for reusable next time as follow code below:
extension UITextView{
func textRangeFromNSRange(range:NSRange)->String{
let myNSString = self.text as NSString
return myNSString.substring(with: range)
}
}
Usage:
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
if (URL.absoluteString == kURLString) {
alert(value: textView.textRangeFromNSRange(range: characterRange))
}
return false
}
If you want to get value from your dictionary you can do like below:
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
if (URL.absoluteString == kURLString) {
alert(value: dictionary[textView.textRangeFromNSRange(range: characterRange)]!)
}
return false
}
Noted: Make sure links in textViews are selectable but not editable.
Links in text views are interactive only if the text view is selectable but noneditable.
set editable false & selectable true:
text.isEditable = false
text.isSelectable = true
To get selected text from textview which is key in your case use below function:
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
if let key = textView.text.substring(with: characterRange){
if let value = dictionary[String(key)]{
print("text :",value)
alert(value: value)
}
}
return false
}
Using extension for string to get Substring with range:
extension String {
func substring(with nsrange: NSRange) -> Substring? {
guard let range = Range(nsrange, in: self) else { return nil }
return self[range]
}
}