I have added a uitextview which is initially non editable. I added a tap gesture which enable the editing to true. In the tap gesture selector I get the word that is being tapped. I have tried a lot many solution but none worked for me as a complete solution. Every solution worked if the textview is not scrolled. But if I scroll the textview the exact word is not retrieved. Here is my code for getting the tapped word:
#objc func handleTap(_ sender: UITapGestureRecognizer) {
notesTextView.isEditable = true
notesTextView.textColor = UIColor.white
if let textView = sender.view as? UITextView {
var pointOfTap = sender.location(in: textView)
print("x:\(pointOfTap.x) , y:\(pointOfTap.y)")
let contentOffsetY = textView.contentOffset.y
pointOfTap.y += contentOffsetY
print("x:\(pointOfTap.x) , y:\(pointOfTap.y)")
word(atPosition: pointOfTap)
}
func word(atPosition: CGPoint) -> String? {
if let tapPosition = notesTextView.closestPosition(to: atPosition) {
if let textRange = notesTextView.tokenizer.rangeEnclosingPosition(tapPosition , with: .word, inDirection: 1) {
let tappedWord = notesTextView.text(in: textRange)
print("Word: \(tappedWord)" ?? "")
return tappedWord
}
return nil
}
return nil
}
EDITED:
Here is the demo project with the problem.
https://github.com/amrit42087/TextViewDemo
The best and easiest way in Swift 4
METHOD 1:
Step 1: Add Tap Gesture on the textview
let tap = UITapGestureRecognizer(target: self, action: #selector(tapResponse(recognizer:)))
textViewTC.addGestureRecognizer(tap)
Step 2: Implement Tap Gesture
#objc func tapResponse(recognizer: UITapGestureRecognizer) {
let location: CGPoint = recognizer.location(in: textViewTC)
let position: CGPoint = CGPoint(x: location.x, y: location.y)
let tapPosition: UITextPosition = textViewTC.closestPosition(to: position)!
guard let textRange: UITextRange = textViewTC.tokenizer.rangeEnclosingPosition(tapPosition, with: UITextGranularity.word, inDirection: 1) else {return}
let tappedWord: String = textViewTC.text(in: textRange) ?? ""
print("tapped word ->", tappedWord)
}
And yes thats it. Go for it.
METHOD 2:
The alternate way is that you can enable links for textview and then set the same as an attribute. Here is an example
var foundRange = attributedString.mutableString.range(of: "Terms of Use") //mention the parts of the attributed text you want to tap and get an custom action
attributedString.addAttribute(NSAttributedStringKey.link, value: termsAndConditionsURL, range: foundRange)
set this attribute text to Textview and textView.delegate = self
Now you just need to handle the response in
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
Hope it helps you. All the best.
You don't need to add the content offset of the text view. When you convert location into a scrollview it will already take its content offset into account.
Removing:
let contentOffsetY = textView.contentOffset.y
pointOfTap.y += contentOffsetY
should work.
Please Try This
//add UITextViewDelegate
let termsAndConditionsURL = "someText"
let privacyURL = "SampleText"
#IBOutlet weak var terms: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
self.terms.delegate = self
// Adding Attributed text to TextView
let str = "By registering, you agree to the Terms and the User Privacy Statement."
let attributedString = NSMutableAttributedString(string: str)
var foundRange = attributedString.mutableString.range(of: "Terms")
attributedString.addAttribute(NSAttributedStringKey.foregroundColor, value: UIColor.blue, range: foundRange)
attributedString.addAttribute(NSAttributedStringKey.underlineStyle , value: NSUnderlineStyle.styleSingle.rawValue, range: foundRange)
attributedString.addAttribute(NSAttributedStringKey.link, value: termsAndConditionsURL, range: foundRange)
foundRange = attributedString.mutableString.range(of: "User Privacy Statement")
attributedString.addAttribute(NSAttributedStringKey.foregroundColor, value: UIColor.blue, range: foundRange)
attributedString.addAttribute(NSAttributedStringKey.underlineStyle , value: NSUnderlineStyle.styleSingle.rawValue, range: foundRange)
attributedString.addAttribute(NSAttributedStringKey.link, value: privacyURL, range: foundRange)
terms.attributedText = attributedString
// Do any additional setup after loading the view.
}
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool
{
if (URL.absoluteString == termsAndConditionsURL)
{
// Perform Terms Action here
} else if (URL.absoluteString == privacyURL)
{
// Perform Terms Action here
}
return false
}
Related
The build-in keys all works well, it will follow the existing text after edit (insert/replace/paste)
But when I doing it with custom NSAttributedString.Key, the attribute doesn't follow.
tagtest is a custom NSAttributedString.Key
extension NSAttributedString.Key {
static let drawTag: NSAttributedString.Key = .init("drawTag")
}
And I draw it by custom NSLayoutManager (similar style with drawing background, but I do need a custom one)
func drawTag(forGlyphRange glyphRange: NSRange,
fillColor:UIColor,
lineFragmentRect lineRect: CGRect,
lineFragmentGlyphRange lineGlyphRange: NSRange,
containerOrigin: CGPoint) {
if let textContainer = textContainer(forGlyphAt: glyphRange.location, effectiveRange: nil) {
let range = NSIntersectionRange(glyphRange, lineGlyphRange)
let lineRect = self.boundingRect(forGlyphRange: range, in: textContainer)
let path = UIBezierPath(roundedRect: lineRect, cornerRadius: 3)
fillColor.setFill()
path.fill()
}
}
I tried to override the NSTextStorage
The attribute can be read but still missed after edit
override func replaceCharacters(in range: NSRange, with str: String) {
beginEditing()
var str = str
var attrs: [NSAttributedString.Key:Any] = [:]
if range.location < string.count{
var r = range
attrs = attributes(at: range.location, effectiveRange: nil)
str = str + "\((attrs[NSAttributedString.Key.drawTag]))"
}
container.replaceCharacters(in: range, with: NSAttributedString(string: str, attributes: attrs))
//container.addAttributes(attrs, range: NSMakeRange(range.location, str.count))
edited([.editedAttributes, .editedCharacters], range: range, changeInLength: (str as NSString).length - range.length)
endEditing()
}
Or override the UITextViewDelegate
This is the most closest one, but cursor will jump to the end, and there will be error when input texts at end.
func textView(_ uiView: UITextView, shouldChangeTextIn: NSRange, replacementText: String) -> Bool{
let attrs = attributedText.attributes(at: shouldChangeTextIn.location, effectiveRange: nil)
attributedText.replaceCharacters(in: shouldChangeTextIn, with: NSAttributedString(string: replacementText, attributes: attrs))
textView.configure(uiView)
return false
}
I'm working with a TextView and I wanted to create two different links within a text where the user accepts the terms and conditions and the privacy policy.
Also I need each link to open a different UIViewController.
Can anyone help me with an example to understand how to achieve this?
I need to understand how to create two Hyper links and how to open them in two different ViewControllers
Thank you all for any help you can give me
For example ... I would like to get a TextView similar to this
You can use the following UITextView delegate Method and Attributed string Tested on swift 5.1 :
let attributedString = NSMutableAttributedString(string: "By continueing you agree terms and conditions and the privacy policy")
attributedString.addAttribute(.link, value: "terms://termsofCondition", range: (attributedString.string as NSString).range(of: "terms and conditions"))
attributedString.addAttribute(.link, value: "privacy://privacypolicy", range: (attributedString.string as NSString).range(of: "privacy policy"))
textView.linkTextAttributes = [ NSAttributedString.Key.foregroundColor: UIColor.blue]
textView.attributedText = attributedString
textView.delegate = self
textView.isSelectable = true
textView.isEditable = false
textView.delaysContentTouches = false
textView.isScrollEnabled = false
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
if URL.scheme == "terms" {
//push view controller 1
return false
} else if URL.scheme == "privacy"{
// pushViewcontroller 2
return false
}
return true
// let the system open this URL
}
The UITextView call this function if the user taps or longPresses the URL link. Implementation of this method is optional. By default, the UITextview opens those applications which are responsible for handling the URL type and pass them the URL. You can use this method to trigger an alternative action
Set your textView properties like this.
textView.attributedText = "By Continuing, you aggree to terms <a href='http://termsandservicelink'>Terms Of Services</a> and <a href='https://privacypolicylink'>Privacy Policy</a>".convertHtml()
textView.isEditable = false
textView.dataDetectorTypes = [.link]
textView.linkTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.blue, NSAttributedString.Key.underlineColor: UIColor.clear]
You can handle tap event on your link in this delegate.
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
//Check your url whether it is privacy policy or terms and do accordigly
return true
}
Here is String extension.
extension String{
func convertHtml() -> NSAttributedString{
guard let data = data(using: .utf8) else { return NSAttributedString() }
do{
return try NSAttributedString(data: data, options: [NSAttributedString.DocumentReadingOptionKey.documentType : NSAttributedString.DocumentType.html, NSAttributedString.DocumentReadingOptionKey.characterEncoding: String.Encoding.utf8.rawValue], documentAttributes: nil)
}catch{
return NSAttributedString()
}
}
}
This result is reached using NSAttributedString, Using NSAttributedString, we can style the text,
myLabel.text = "By signing up you agree to our Terms & Conditions and Privacy Policy"
let text = (myLabel.text)!
let underlineAttriString = NSMutableAttributedString(string: text)
let range1 = (text as NSString).rangeOfString("Terms & Conditions")
underlineAttriString.addAttribute(NSUnderlineStyleAttributeName, value: NSUnderlineStyle.StyleSingle.rawValue, range: range1)
let range2 = (text as NSString).rangeOfString("Privacy Policy")
underlineAttriString.addAttribute(NSUnderlineStyleAttributeName, value: NSUnderlineStyle.StyleSingle.rawValue, range: range2)
myLabel.attributedText = underlineAttriString
Extend UITapGestureRecognizer to provide a convenient function to detect if a certain range (NSRange) is tapped on in a UILabel.
extension UITapGestureRecognizer {
func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
// Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: CGSize.zero)
let textStorage = NSTextStorage(attributedString: label.attributedText!)
// Configure layoutManager and textStorage
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
// Configure textContainer
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = label.lineBreakMode
textContainer.maximumNumberOfLines = label.numberOfLines
let labelSize = label.bounds.size
textContainer.size = labelSize
// Find the tapped character location and compare it to the specified range
let locationOfTouchInLabel = self.locationInView(label)
let textBoundingBox = layoutManager.usedRectForTextContainer(textContainer)
let textContainerOffset = CGPointMake((labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
(labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y);
let locationOfTouchInTextContainer = CGPointMake(locationOfTouchInLabel.x - textContainerOffset.x,
locationOfTouchInLabel.y - textContainerOffset.y);
let indexOfCharacter = layoutManager.characterIndexForPoint(locationOfTouchInTextContainer, inTextContainer: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
return NSLocationInRange(indexOfCharacter, targetRange)
}
}
UITapGestureRecognizer send action to tapLabel:, and detect using the extension method didTapAttributedTextInLabel:inRange:.
#IBAction func tapLabel(gesture: UITapGestureRecognizer) {
let text = (myLabel.text)!
let termsRange = (text as NSString).rangeOfString("Terms & Conditions")
let privacyRange = (text as NSString).rangeOfString("Privacy Policy")
if gesture.didTapAttributedTextInLabel(myLabel, inRange: termsRange) {
print("Tapped terms")
} else if gesture.didTapAttributedTextInLabel(myLabel, inRange: privacyRange)
{
print("Tapped privacy")
} else {
print("Tapped none")
}
}
Let's say I have the following UITextView object:
var textView = UITextView()
textView.text = "Hello World!"
Now let's say I don't want to allow the user to delete the "W" character while editing it. How could I know which character is before the cursor (or selected by it)?
I'm looking for something that would work like this:
if textView.characterBeforeCursor() != "W" {
textView.deleteBackward()
}
or... (when the user selects the "W" character):
if textView.selectedTextContains("W") == false {
textView.deleteBackward()
}
What approach should I use to accomplish this?
Here's an idea, not fully tested, but seems to work... Just grab the character about to be acted upon and block backspace if its the target... Also with regard to selection of text, if the selection contains the target at all, we block new text.
import UIKit
class ViewController: UIViewController, UITextViewDelegate {
#IBOutlet weak var textView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
self.textView.delegate = self
// Do any additional setup after loading the view, typically from a nib.
}
func characterBeforeCursor() -> String? {
// get the cursor position
if let cursorRange = textView.selectedTextRange {
// get the position one character before the cursor start position
if let newPosition = textView.position(from: cursorRange.start, offset: -1) {
let range = textView.textRange(from: newPosition, to: cursorRange.start)
return textView.text(in: range!)
}
}
return nil
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if (characterBeforeCursor() == "W") {
let char = text.cString(using: String.Encoding.utf8)!
let isBackSpace = strcmp(char, "\\b")
if (isBackSpace == -92) {
return false
}
return true
}
else {
if let range = textView.selectedTextRange {
let selectedText = textView.text(in: range)
if (selectedText!.contains("W")) {
return false
}
}
return true
}
}
}
This should do it:
let forbiddenLetter = "W"
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
guard let txt = textView.text, let txtRange = Range(range, in: txt) else {
return false
}
let subString: Substring = txt[txtRange]
return !subString.contains(forbiddenLetter)
}
In the code above let txt = textView.text is just for simplicity, we could keep force-unwrapping textView.text! since the .text property is designed never returns nil for a non-nil UITextView.
By let txtRange = Range(range, in: txt) we get a variable of type Range<String.Index> instead of the vanilla NSRange that range is. By doing so we can get the Substring of txt that the textView is about to change.
Finally, the result of checking whether or not the subString contains the forbiddenLetter, is returned.
This snippet would prevent deleting W by using:
Backspace key ⌫
Deleting selection
Pasting over selection
Autocorrect (from the popup)
I am trying to display an attributed string in a UITextview with clickable links. I've created a simple test project to see where I'm going wrong and still can't figure it out. I've tried enabling user interaction and setting the shouldInteractWithURLs delegate method, but it's still not working. Here's my code (for a view controller that only contains a textview)
#IBOutlet weak var textView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let string = "Google"
let linkString = NSMutableAttributedString(string: string)
linkString.addAttribute(NSLinkAttributeName, value: NSURL(string: "https://www.google.com")!, range: NSMakeRange(0, string.characters.count))
linkString.addAttribute(NSFontAttributeName, value: UIFont(name: "HelveticaNeue", size: 25.0)!, range: NSMakeRange(0, string.characters.count))
textView.attributedText = linkString
textView.delegate = self
textView.selectable = true
textView.userInteractionEnabled = true
}
And here are the delegate methods I've implemented:
func textViewShouldBeginEditing(textView: UITextView) -> Bool {
return false
}
func textView(textView: UITextView, shouldInteractWithURL URL: NSURL, inRange characterRange: NSRange) -> Bool {
return true
}
This still isn't working. I've searched on this topic and nothing has helped yet. Thanks so much in advance.
Just select the UITextView in your storyboard and go to "Show Attributes inspector" and select selectable and links. As the image below shows. Make sure Editable is unchecked.
For swift3.0
override func viewDidLoad() {
super.viewDidLoad()
let linkAttributes = [
NSLinkAttributeName: NSURL(string: "http://stalwartitsolution.co.in/luminutri_flow/terms-condition")!
] as [String : Any]
let attributedString = NSMutableAttributedString(string: "Please tick box to confirm you agree to our Terms & Conditions, Privacy Policy, Disclaimer. ")
attributedString.setAttributes(linkAttributes, range: NSMakeRange(44, 18))
attributedString.addAttribute(NSUnderlineStyleAttributeName, value: NSNumber(value: 1), range: NSMakeRange(44, 18))
textview.delegate = self
textview.attributedText = attributedString
textview.linkTextAttributes = [NSForegroundColorAttributeName: UIColor.red]
textview.textColor = UIColor.white
}
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
return true
}
Swift 3 iOS 10: Here's Clickable extended UITextView that detect websites inside the textview automatically as long as the link start with www. for example: www.exmaple.com if it exist anywhere in the text will be clickable. Here's the class:
import Foundation
import UIKit
public class ClickableTextView:UITextView{
var tap:UITapGestureRecognizer!
override public init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
print("init")
setup()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup(){
// Add tap gesture recognizer to Text View
tap = UITapGestureRecognizer(target: self, action: #selector(self.myMethodToHandleTap(sender:)))
// tap.delegate = self
self.addGestureRecognizer(tap)
}
func myMethodToHandleTap(sender: UITapGestureRecognizer){
let myTextView = sender.view as! UITextView
let layoutManager = myTextView.layoutManager
// location of tap in myTextView coordinates and taking the inset into account
var location = sender.location(in: myTextView)
location.x -= myTextView.textContainerInset.left;
location.y -= myTextView.textContainerInset.top;
// character index at tap location
let characterIndex = layoutManager.characterIndex(for: location, in: myTextView.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
// if index is valid then do something.
if characterIndex < myTextView.textStorage.length {
let orgString = myTextView.attributedText.string
//Find the WWW
var didFind = false
var count:Int = characterIndex
while(count > 2 && didFind == false){
let myRange = NSRange(location: count-1, length: 2)
let substring = (orgString as NSString).substring(with: myRange)
// print(substring,count)
if substring == " w" || (substring == "w." && count == 3){
didFind = true
// print("Did find",count)
var count2 = count
while(count2 < orgString.characters.count){
let myRange = NSRange(location: count2 - 1, length: 2)
let substring = (orgString as NSString).substring(with: myRange)
// print("Did 2",count2,substring)
count2 += 1
//If it was at the end of textView
if count2 == orgString.characters.count {
let length = orgString.characters.count - count
let myRange = NSRange(location: count, length: length)
let substring = (orgString as NSString).substring(with: myRange)
openLink(link: substring)
print("It's a Link",substring)
return
}
//If it's in the middle
if substring.hasSuffix(" "){
let length = count2 - count
let myRange = NSRange(location: count, length: length)
let substring = (orgString as NSString).substring(with: myRange)
openLink(link: substring)
print("It's a Link",substring)
return
}
}
return
}
if substring.hasPrefix(" "){
print("Not a link")
return
}
count -= 1
}
}
}
func openLink(link:String){
if let checkURL = URL(string: "http://\(link.replacingOccurrences(of: " ", with: ""))") {
if UIApplication.shared.canOpenURL(checkURL) {
UIApplication.shared.open(checkURL, options: [:], completionHandler: nil)
print("url successfully opened")
}
} else {
print("invalid url")
}
}
public override func didMoveToWindow() {
if self.window == nil{
self.removeGestureRecognizer(tap)
print("ClickableTextView View removed from")
}
}
}
I'm trying to color all occurrences (not just the first) of a textview string however when I get to the looping I get a cannot invoke rangeOfString argument.
I checked the rangeOfString:options:range documentation on Swift 2 and it looked pretty similar. I'm not really sure what I'm doing wrong.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var textView: UITextView!
func textView(textView: UITextView){
if textView == self.textView {
let nsString = textView.text as NSString
let stringLength = textView.text.characters.count
var range = NSRange(location: 0, length: textView.text.characters.count)
let searchString = textView.text
let text = NSMutableAttributedString(string: textView.text)
text.addAttribute(NSForegroundColorAttributeName, value: UIColor.redColor(), range: NSMakeRange(0, stringLength))
textView.attributedText = text
while(range.location != NSNotFound) {
range = (textView.text as NSString).rangeOfString(searchString, options: NSStringCompareOptions, range: range)
text.addAttribute(NSForegroundColorAttributeName, value: UIColor.redColor(), range: nsString.rangeOfString("hello"))
}
}
}
}
It looks like you try to highlight user input from search bar. Here how I do so:
func highlightedText(text: NSString, inText: NSString, var withColor: UIColor?) -> NSMutableAttributedString {
var attributedString = NSMutableAttributedString(string:inText as String)
let range = (inText.lowercaseString as NSString).rangeOfString(text.lowercaseString as String)
if withColor == nil {
if let color = globalTint() {
withColor = color
} else {
withColor = UIColor.redColor()
}
}
attributedString.addAttribute(NSForegroundColorAttributeName, value: withColor! , range: range)
return attributedString
}
...
textView.attributedText = highlightedText("string", inText: titleText, color: nil)