简体   繁体   中英

How to display rounded gray background by tapping specific characters in UITextView?

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

NSAttributedString background color and rounded corners

How to set NSString's background cornerRadius on iOS7

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:

  • Additionally, you should keep a reference to layers created that way, so that you can remove or reframe them.
  • 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM