Как отобразить округлый серый фон, нажав определенные символы в UITextView?

Я пытаюсь отобразить текст с помощью UITextView. Я добавил «Узнать больше» при отображении длинных текстов. Я хочу изменить цвет фона при нажатии. Я установил фон NSAttributedString, но не могу правильно установить скругленные углы и поля.

Спасибо!

Что я хочу делать!

Серый фон с достаточным полем с закругленным углом при нажатии символа, добавленного в UITextView.

Примечание: уже можно нажать на символ. Этот вопрос про эффект при постукивании.

См. еще пример

Пример пересечения линий

Аналогичный вопрос

Цвет фона NSAttributedString и закругленные углы

Как установить радиус угла фона NSString в iOS7


person Diricawl    schedule 20.02.2018    source источник
comment
После нажатия кнопки «Показать больше» вы сможете увидеть полный текст. Нужно показать оставшийся текст с цветом фона?   -  person McDonal_11    schedule 20.02.2018
comment
@ McDonal_11 Это эффект распознавания крана. То есть, когда вы касаетесь (touchesBegan), он становится серым фоном, а когда вы его отпускаете, он исчезает (touchesEnded). См. Еще пример. Этот вопрос касается эффекта при нажатии символов UITextView.m Как этого добиться с помощью TextKit?   -  person Diricawl    schedule 20.02.2018
comment
Вы использовали TapGesture для TextView? или как ты пытаешься?   -  person McDonal_11    schedule 20.02.2018
comment
@ McDonal_11 Спасибо за ваш комментарий. Я использую Hittest для распознавания касаний. Я могу получить положение касания и определить, был ли нажат целевой символ. Чего я не могу сделать, так это добавить эффект серого фона (закругленный угол, размер с полем) при нажатии на целевой символ. Прикрепленное изображение - это «Узнать больше» и «Ссылка на снимок экрана приложения Facebook для iOS» при нажатии. Пример, в котором серый фон настроен правильно даже при разрыве строки.   -  person Diricawl    schedule 20.02.2018
comment
Я пересеклась с серым цветовым уровнем. Примерка закругленных углов.   -  person McDonal_11    schedule 20.02.2018
comment
Я обновил свой ответ. пожалуйста, проверьте это.   -  person McDonal_11    schedule 20.02.2018
comment
@ Cœur Да. Его нельзя прокручивать / редактировать.   -  person Diricawl    schedule 29.03.2018


Ответы (2)


Добавление цвета фона с закругленными углами в текст UITextView. Этот answer даст вам несколько идей для вашего Question.

Логика:

В UITextView я добавил UITapGestureRecognizer, который определяет действие касания пользователя Character по Character. Если пользователь нажмет на любой из символов в subString, будет создан новый UIView и сработает таймер. Когда таймер истечет, созданный UIView будет удален из UITextView.

С помощью myTextView.position мы можем получить CGRect подстроки. Это рамка для Created UIView. Size (WIDTH) для каждого слова в subString, можно получить из 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()
            }
        }
    }

}

Все попытки перепробовал, по увеличению Font Size.

Попытка 1

введите здесь описание изображения

Попытка 2

введите здесь описание изображения

Попытка 3

введите здесь описание изображения

person McDonal_11    schedule 20.02.2018
comment
Поскольку я продолжаю показывать фон, пока не отпущу его, я буду управлять им с помощью сенсорного действия вместо таймера. Использование UIView - одно из решений. В качестве альтернативного решения я пытаюсь реализовать это, используя подкласс NSLayoutManager. Эта идея пришла из этого вопроса. Как насчет этого? Я также открыл новый вопрос, связанный с этим. - person Diricawl; 21.02.2018

Если вопрос заключается в получении диапазона нажатого символа, вы должны использовать 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
}

Вы также можете использовать другой UIGestureRecognizer в зависимости от ваших потребностей.

Если ваш вопрос касается того, как нарисовать собственный фон для диапазона текста, вы можете использовать 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
}

Примечания:

  • Кроме того, вы должны сохранить ссылку на созданные таким образом слои, чтобы вы могли их удалить или переформатировать.
  • layoutManager.boundingRect(forGlyphRange:in:) может не дать вам того, что вы хотите, если диапазон состоит из нескольких строк, поэтому при необходимости рассмотрите возможность разделения диапазона.
person Cœur    schedule 08.04.2018