4

This is a follow up to this question How can I change style of some words in my UITextView one by one in Swift?

Thanks to @Josh's help I was able to write a piece of code that highlights each word that begins with # - and do it one by one. My final code for that was:

func highlight (to index: Int) {

    let regex = try? NSRegularExpression(pattern: "#(\\w+)", options: [])
    let matches = regex!.matches(in: hashtagExplanationTextView.text, options: [], range: NSMakeRange(0, (hashtagExplanationTextView.text.characters.count)))
    let titleDict: NSDictionary = [NSForegroundColorAttributeName: orangeColor]
    let titleDict2: NSDictionary = [NSForegroundColorAttributeName: UIColor.red]
    let storedAttributedString = NSMutableAttributedString(string: hashtagExplanationTextView.text!, attributes: titleDict as! [String : AnyObject])


    let attributedString = NSMutableAttributedString(attributedString: storedAttributedString)
    guard index < matches.count else {
        return
    }

    for i in 0..<index{
        let matchRange = matches[i].rangeAt(0)
        attributedString.addAttributes(titleDict2 as! [String : AnyObject], range: matchRange)
    }
    hashtagExplanationTextView.attributedText = attributedString
    if #available(iOS 10.0, *) {
        let _ = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in
            self.highlight(to: index + 1)
        }
    } else {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            self.highlight(to: index + 1)
        }
    }
}

This works fine, but I would like to change the logic so that it does not highlight the # words, but highlights (one by one) words from preselected array of those words.

So I have this array var myArray:[String] = ["those","words","are","highlighted"] and how can I put it instead of regex match in my code?

Community
  • 1
  • 1
user3766930
  • 5,629
  • 10
  • 51
  • 104
  • What have you tried so far in terms of matching against the array of words? – Patrick Goley Nov 12 '16 at 15:56
  • @Patrick since my list of words is quite short (I have like 5 or 6 of them) I tried to modify the regex pattern, but it didn't work well since other words were also caught by that. I also tried to provide the words directly to `matches`, but then the further logic didn't work – user3766930 Nov 12 '16 at 15:58

3 Answers3

3

I believe you are using regex to get an array of NSRange. Here, you need a slightly different datastructure like [String : [NSRange]]. Then you can use rangeOfString function to detect the NSRange where the word is located. You can follow the example given below for that:

let wordMatchArray:[String] = ["those", "words", "are", "highlighted"]
let labelText:NSString = NSString(string: "those words, those ldsnvldnvsdnds, are, highlighted,words are highlighted")
let textLength:Int = labelText.length

var dictionaryForEachWord:[String : [NSRange]] = [:]

for eachWord:String in wordMatchArray {

   var prevRange:NSRange = NSMakeRange(0, 0)
   var rangeArray:[NSRange] = []

   while ((prevRange.location + prevRange.length) < textLength) {

      let start:Int = (prevRange.location + prevRange.length)
      let rangeEach:NSRange = labelText.range(of: eachWord, options: NSString.CompareOptions.literal, range: NSMakeRange(start, textLength-start))
      if rangeEach.length == 0 {
         break
      }
      rangeArray.append(rangeEach)
      prevRange = rangeEach
   }

   dictionaryForEachWord[eachWord] = rangeArray
}

Now that you have an array of NSRange i.e, [NSRange] for each word stored in a dictionary, you can highlight each word accordingly in your UITextView.

Feel free to comment if you have any doubts regarding the implementation :)

KrishnaCA
  • 5,615
  • 1
  • 21
  • 31
0

For this new requirement you don't need a regex, you can just iterate over your array of words and use rangeOfString to find out if that string exists and set the attributes for the located range.

To match the original functionality, after you find a matching range you need to search again, starting from the end of that range, to see if there is another match later in your source text.

Wain
  • 118,658
  • 15
  • 128
  • 151
0

The proposed solutions so far suggest that you go through each word and then search them in the text view. This works, but you are traversing the text way too many times.

What I would suggest is to enumerate all the words in the text and see if they match any of the words to highlight:

class ViewController: UIViewController {

    @IBOutlet var textView: UITextView!

    override func viewDidLoad() {
        super.viewDidLoad()
        textView.delegate = self
        highlight()
    }

    func highlight() {
        guard let attributedText = textView.attributedText else {
            return
        }
        let wordsToHighlight = ["those", "words", "are", "highlighted"]
        let text = NSMutableAttributedString(attributedString: attributedText)
        let textRange = NSRange(location: 0, length: text.length)
        text.removeAttribute(NSForegroundColorAttributeName, range: textRange)

        (text.string as NSString).enumerateSubstrings(in: textRange, options: [.byWords]) { [weak textView] (word, range, _, _) in
            guard let word = word else { return }
            if wordsToHighlight.contains(word) {
                textView?.textStorage.setAttributes([NSForegroundColorAttributeName: UIColor.red], range: range)
            } else {
                textView?.textStorage.removeAttribute(NSForegroundColorAttributeName, range: range)
            }
        }
        textView.typingAttributes.removeValue(forKey: NSForegroundColorAttributeName)
    }
}

extension ViewController: UITextViewDelegate {
    func textViewDidChange(_ textView: UITextView) {
        highlight()
    }
}

This should be fine for small texts. For long texts, going through everything on each change can really hurt performance. In that case, I'd recommend using a custom NSTextStorage subclass. There you would have better control over what range of text have changed and apply the highlight only to that section.

Jorge Bernal
  • 265
  • 3
  • 11