I am trying to display text using UITextView. I added "See More" when displaying long texts. I would like to change the background color when tapping it. I set the background of NSAttributedString, but I can not set round corners and margins well.
Thanks!
What I want to do!
A gray background with a sufficient margin with a rounded corner when tapping the character added to UITextView.
Note: It is already possible to tap a character. This question is about the effect at tapping.
Similar question
Adding Background Color with rounded corners in UITextView's Text. This answer
will give some ideas for your Question
.
Logic:
In UITextView
, I have added UITapGestureRecognizer
, which detects user's tap action Character
by Character
. If user, taps on any one of Character in subString
, new UIView
will be created and triggering Timer. When timer gets end, created UIView will be removed from UITextView.
With the help of, myTextView.position
, we can get subString's CGRect
. That is frame for Created UIView
. Size (WIDTH)
for each words in subString
, can get from SizeAtrributes
.
@IBOutlet weak var challengeTextVw: UITextView!
let fullText = "We Love Swift and Swift attributed text "
var myString = NSMutableAttributedString ()
let subString = " Swift attributed text "
var subStringSizesArr = [CGFloat]()
var myRange = NSRange()
var myWholeRange = NSRange()
let fontSize : CGFloat = 25
var timerTxt = Timer()
let delay = 3.0
override func viewDidLoad() {
super.viewDidLoad()
myString = NSMutableAttributedString(string: fullText)
myRange = (fullText as! NSString).range(of: subString)
myWholeRange = (fullText as! NSString).range(of: fullText)
let substringSeperatorArr = subString.components(separatedBy: " ")
print(substringSeperatorArr)
print(substringSeperatorArr.count)
var strConcat = " "
for str in 0..<substringSeperatorArr.count
{
strConcat = strConcat + substringSeperatorArr[str] + " "
let textSize = (strConcat as! NSString).size(withAttributes: [NSAttributedStringKey.font : UIFont.systemFont(ofSize: fontSize)])
print("strConcatstrConcat ", strConcat)
if str != 0 && str != (substringSeperatorArr.count - 2)
{
print("times")
subStringSizesArr.append(textSize.width)
}
}
let myCustomAttribute = [NSAttributedStringKey.init("MyCustomAttributeName") : "some value", NSAttributedStringKey.foregroundColor : UIColor.orange] as [NSAttributedStringKey : Any]
let fontAtrib = [NSAttributedStringKey.font : UIFont.systemFont(ofSize: fontSize)]
myString.addAttributes(myCustomAttribute, range: myRange)
myString.addAttributes(fontAtrib, range: myWholeRange)
challengeTextVw.attributedText = myString
let tap = UITapGestureRecognizer(target: self, action: #selector(myMethodToHandleTap))
tap.delegate = self
challengeTextVw.addGestureRecognizer(tap)
challengeTextVw.isEditable = false
challengeTextVw.isSelectable = false
}
@objc func myMethodToHandleTap(_ sender: UITapGestureRecognizer) {
let myTextView = sender.view as! UITextView
let layoutManager = myTextView.layoutManager
let numberOfGlyphs = layoutManager.numberOfGlyphs
var numberOfLines = 0
var index = 0
var lineRange:NSRange = NSRange()
while (index < numberOfGlyphs) {
layoutManager.lineFragmentRect(forGlyphAt: index, effectiveRange: &lineRange)
index = NSMaxRange(lineRange);
numberOfLines = numberOfLines + 1
}
print("noLin ", numberOfLines)
// 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
{
// print the character index
print("character index: \(characterIndex)")
// print the character at the index
let myRangee = NSRange(location: characterIndex, length: 1)
let substring = (myTextView.attributedText.string as NSString).substring(with: myRangee)
print("character at index: \(substring)")
// check if the tap location has a certain attribute
let attributeName = NSAttributedStringKey.init("MyCustomAttributeName")
let attributeValue = myTextView.attributedText.attribute(attributeName, at: characterIndex, effectiveRange: nil) as? String
if let value = attributeValue
{
print("You tapped on \(attributeName) and the value is: \(value)")
print("\n\n ererereerer")
timerTxt = Timer.scheduledTimer(timeInterval: delay, target: self, selector: #selector(delayedAction), userInfo: nil, repeats: false)
myTextView.layoutManager.ensureLayout(for: myTextView.textContainer)
// text position of the range.location
let start = myTextView.position(from: myTextView.beginningOfDocument, offset: myRange.location)!
// text position of the end of the range
let end = myTextView.position(from: start, offset: myRange.length)!
// text range of the range
let tRange = myTextView.textRange(from: start, to: end)
// here it is!
let rect = myTextView.firstRect(for: tRange!) //firstRectForRange(tRange)
var secondViewWidthIndex = Int()
for count in 0..<subStringSizesArr.count
{
if rect.width > subStringSizesArr[count]
{
secondViewWidthIndex = count
}
}
let backHideVw = UIView()
backHideVw.frame.origin.x = rect.origin.x
backHideVw.frame.origin.y = rect.origin.y + 1
backHideVw.frame.size.height = rect.height
backHideVw.frame.size.width = rect.width
backHideVw.backgroundColor = UIColor.brown
backHideVw.layer.cornerRadius = 2
backHideVw.tag = 10
myTextView.addSubview(backHideVw)
myTextView.sendSubview(toBack: backHideVw)
if numberOfLines > 1
{
let secondView = UIView()
secondView.frame.origin.x = 0
secondView.frame.origin.y = backHideVw.frame.origin.y + backHideVw.frame.size.height
secondView.frame.size.height = backHideVw.frame.size.height
secondView.frame.size.width = (subStringSizesArr.last! - subStringSizesArr[secondViewWidthIndex]) + 2
secondView.backgroundColor = UIColor.brown
secondView.layer.cornerRadius = 2
secondView.tag = 20
backHideVw.frame.size.width = subStringSizesArr[secondViewWidthIndex]
myTextView.addSubview(secondView)
print("secondView.framesecondView.frame ", secondView.frame)
myTextView.sendSubview(toBack: secondView)
}
print("rectrect ", rect)
}
}
}
@objc func delayedAction()
{
for subVws in challengeTextVw.subviews
{
if (String(describing: subVws).range(of:"UIView") != nil)
{
if (subVws as! UIView).tag == 10 || (subVws as! UIView).tag == 20
{
subVws.removeFromSuperview()
}
}
}
}
All attempts tried, by increasing Font Size
.
Attempt 1
Attempt 2
Attempt 3
If the question is about acquiring the range of a tapped character, you should use layoutManager.characterIndex(for:in:fractionOfDistanceBetweenInsertionPoints:)
:
private var selectGestureRecognizer: UITapGestureRecognizer!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
selectGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(textTapped))
addGestureRecognizer(selectGestureRecognizer)
}
@objc func textTapped(recognizer: UITapGestureRecognizer) {
guard recognizer == selectGestureRecognizer else {
return
}
var location = recognizer.location(in: self)
// https://stackoverflow.com/a/25466154/1033581
location.x -= textContainerInset.left
location.y -= textContainerInset.top
let charIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
let range = NSRange(location: charIndex, length: 1)
// do something with the tapped range
}
You can also use a different UIGestureRecognizer depending on your needs.
If your question is about how to draw a custom background for a range of text, you may use layoutManager.boundingRect(forGlyphRange:in:)
:
func layoutBackground(range: NSRange) -> CALayer {
let rect = boundingRectForCharacterRange(range)
let backgroundLayer = CALayer()
backgroundLayer.frame = rect
backgroundLayer.cornerRadius = 5
backgroundLayer.backgroundColor = UIColor.black.cgColor.copy(alpha: 0.2)
layer.insertSublayer(backgroundLayer, at: 0)
return backgroundLayer
}
/// CGRect of substring.
func boundingRectForCharacterRange(_ range: NSRange) -> CGRect {
let layoutManager = self.layoutManager
var glyphRange = NSRange()
// Convert the range for glyphs.
layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange)
var glyphRect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
// https://stackoverflow.com/a/28332722/1033581
glyphRect.origin.x += textContainerInset.left
glyphRect.origin.y += textContainerInset.top
return glyphRect
}
Notes:
layoutManager.boundingRect(forGlyphRange:in:)
may not give you what you want when the range spans on multiple lines, so consider splitting the range when appropriate.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.