electronicwall
electronicwall

Reputation: 55

How to highlight a UITextView's text line by line in swift?

I am trying to highlight line by line the text in a UITextView. I want to iterate over each line and highlight that one for the user to see, then I want to remove the highlighting effect in preparation for the next line. I have tried and failed to create a solution and this is my best chance right now.

Here is some of what I have been working on so far, it currently fills the UITextView with "NSBackgroundColor 1101" for some reason and I do not know why that is.

func highlight() {
        let str = "This is\n some placeholder\n text\nwith newlines."
        var newStr = NSMutableAttributedString(string: "")
        var arr:[String] = str.components(separatedBy: "\n")

        var attArr:[NSMutableAttributedString] = []

        for i in 0..<arr.count {
            attArr.append(NSMutableAttributedString(string: arr[i]))
        }

        for j in 0..<attArr.count {
            let range = NSMakeRange(0, attArr[j].length)
            attArr[j].addAttribute(.backgroundColor, value: UIColor.yellow, range: range)
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5){
                for m in 0..<attArr.count {
                    newStr = NSMutableAttributedString(string: "\(attArr[m])\n")
                    self.textView.attributedText = newStr

                }
            }
            attArr[j].removeAttribute(.backgroundColor, range: range)
            //remove from texview here
        }
    }

As you can see this algorithm is supposed to strip the textView text and place that into an array by separating each line the new line delimiter. The next thing done is to create an array filled with the same text but as mutable attributed string to begin adding the highlight attribute. Each time a highlighted line appears there is a small delay until the next line begins to highlight. If anyone can help or point me in to the right direction to begin implementing this correctly it would help immensely,

Thank you!

Upvotes: 3

Views: 3853

Answers (1)

rob mayoff
rob mayoff

Reputation: 386018

So you want this:

demo

You need the text view's contents to always be the full string, with one line highlighted, but your code sets it to just the highlighted line. Your code also schedules all the highlights to happen at the same time (.now() + 0.5) instead of at different times.

Here's what I'd suggest:

  1. Create an array of ranges, one range per line.

  2. Use that array to modify the text view's textStorage by removing and adding the .backgroundColor attribute as needed to highlight and unhighlight lines.

  3. When you highlight line n, schedule the highlighting of line n+1. This has two advantages: it will be easier and more efficient to cancel the animation early if you need to, and it will be easier to make the animation repeat endlessly if you need to.

I created the demo above using this playground:

import UIKit
import PlaygroundSupport

let text = "This is\n some placeholder\n text\nwith newlines."
let textView = UITextView(frame: CGRect(x: 0, y:0, width: 200, height: 100))
textView.backgroundColor = .white
textView.text = text

let textStorage = textView.textStorage

// Use NSString here because textStorage expects the kind of ranges returned by NSString,
// not the kind of ranges returned by String.
let storageString = textStorage.string as NSString
var lineRanges = [NSRange]()
storageString.enumerateSubstrings(in: NSMakeRange(0, storageString.length), options: .byLines, using: { (_, lineRange, _, _) in
    lineRanges.append(lineRange)
})

func setBackgroundColor(_ color: UIColor?, forLine line: Int) {
    if let color = color {
        textStorage.addAttribute(.backgroundColor, value: color, range: lineRanges[line])
    } else {
        textStorage.removeAttribute(.backgroundColor, range: lineRanges[line])
    }
}

func scheduleHighlighting(ofLine line: Int) {
    DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
        if line > 0 { setBackgroundColor(nil, forLine: line - 1) }
        guard line < lineRanges.count else { return }
        setBackgroundColor(.yellow, forLine: line)
        scheduleHighlighting(ofLine: line + 1)
    }
}

scheduleHighlighting(ofLine: 0)

PlaygroundPage.current.liveView = textView

Upvotes: 1

Related Questions