Color all occurrences of string in swift
Asked Answered
B

6

20

This code

var textSearch="hi"
var textToShow="hi hihi hi" 
var rangeToColor = (textToShow as NSString).rangeOfString(textSearch)
var attributedString = NSMutableAttributedString(string:textToShow)
attributedString.addAttribute(NSForegroundColorAttributeName, value: UIColor.yellowColor() , range: rangeToColor)
TextView.attributedText=attributedString

gives me NSRange to color a string inside the TextView. The problem is that I only returns the first occurrence. If the word contains "hi hihi hi" only the first "hi" is colored. How can I get all occurrences of the string?

Billups answered 28/11, 2014 at 0:0 Comment(2)
Your solution is in objective-C :/Billups
Wrote in Swift from #8534191Aribold
A
31

Swift 5

let attrStr = NSMutableAttributedString(string: "hi hihi hey")
let inputLength = attrStr.string.count
let searchString = "hi"
let searchLength = searchString.characters.count
var range = NSRange(location: 0, length: attrStr.length)

while (range.location != NSNotFound) {
    range = (attrStr.string as NSString).range(of: searchString, options: [], range: range)
    if (range.location != NSNotFound) {
        attrStr.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.yellow, range: NSRange(location: range.location, length: searchLength))
        range = NSRange(location: range.location + range.length, length: inputLength - (range.location + range.length))
    }
}

Swift 3

let attrStr = NSMutableAttributedString(string: "hi hihi hey")
let inputLength = attrStr.string.characters.count
let searchString = "hi"
let searchLength = searchString.characters.count
var range = NSRange(location: 0, length: attrStr.length)

while (range.location != NSNotFound) {
    range = (attrStr.string as NSString).range(of: searchString, options: [], range: range)
    if (range.location != NSNotFound) {
        attrStr.addAttribute(NSForegroundColorAttributeName, value: UIColor.yellow(), range: NSRange(location: range.location, length: searchLength))
        range = NSRange(location: range.location + range.length, length: inputLength - (range.location + range.length))
    }
}

Swift 2

let attrStr = NSMutableAttributedString(string: "hi hihi hey")
let inputLength = attrStr.string.characters.count
let searchString = "hi"
let searchLength = searchString.characters.count
var range = NSRange(location: 0, length: attrStr.length)

while (range.location != NSNotFound) {
    range = (attrStr.string as NSString).rangeOfString(searchString, options: [], range: range)
    if (range.location != NSNotFound) {
        attrStr.addAttribute(NSForegroundColorAttributeName, value: UIColor.yellowColor(), range: NSRange(location: range.location, length: searchLength))
        range = NSRange(location: range.location + range.length, length: inputLength - (range.location + range.length))
    }
}
Aribold answered 28/11, 2014 at 1:16 Comment(1)
In Swift 5 we should use count instead of characters.count. As result searchLength will look as follows: let searchLength = searchString.countSihon
S
5

Swift 4:

let string = "foo fbar foofoo foofo"
let mutableAttributedString = NSMutableAttributedString(string: string)
let searchString = "foo"
var rangeToSearch = string.startIndex..<string.endIndex
while let matchingRange = string.range(of: searchString, options: [], range: rangeToSearch) {
  mutableAttributedString.addAttribute(.foregroundColor, value: UIColor.yellow, range: NSRange(matchingRange, in: string))
  rangeToSearch = matchingRange.upperBound..<string.endIndex
}
Staurolite answered 18/4, 2018 at 15:31 Comment(0)
N
4

Syntax sugar for Kevin's answer above.

Called like:

attrStr.attributeRangeFor(searchString, attributeValue: UIColor.yellowColor(), atributeSearchType: .All)

Swift 2.0:

import UIKit

extension NSMutableAttributedString {
    enum AtributeSearchType {
        case First, All, Last
    }

    func attributeRangeFor(searchString: String, attributeValue: AnyObject, atributeSearchType: AtributeSearchType) {
        let inputLength = self.string.characters.count
        let searchLength = searchString.characters.count
        var range = NSRange(location: 0, length: self.length)
        var rangeCollection = [NSRange]()

        while (range.location != NSNotFound) {
            range = (self.string as NSString).rangeOfString(searchString, options: [], range: range)
            if (range.location != NSNotFound) {
                switch atributeSearchType {
                case .First:
                    self.addAttribute(NSForegroundColorAttributeName, value: attributeValue, range: NSRange(location: range.location, length: searchLength))
                    return
                case .All:
                    self.addAttribute(NSForegroundColorAttributeName, value: attributeValue, range: NSRange(location: range.location, length: searchLength))
                    break
                case .Last:
                    rangeCollection.append(range)
                    break
                }

                range = NSRange(location: range.location + range.length, length: inputLength - (range.location + range.length))
            }
        }

        switch atributeSearchType {
        case .Last:
            let indexOfLast = rangeCollection.count - 1
            self.addAttribute(NSForegroundColorAttributeName, value: attributeValue, range: rangeCollection[indexOfLast])
            break
        default:
            break
        }
    }
}
Needful answered 3/3, 2016 at 11:22 Comment(2)
For the purpose of improving my answers in the future can some explain why my answer has been down voted? Using an extension method seems cleaner to me.Needful
Thanks! Found this a very useful and extensible way for my use case. Perhaps a dictionary could be used for multiple attributes?Gossipy
A
2

I made those two methods to either color for only once occurrence or color all the occurrence for that text:

extension NSMutableAttributedString{
    func setColorForText(_ textToFind: String, with color: UIColor) {
        let range = self.mutableString.range(of: textToFind, options: .caseInsensitive)
        if range.location != NSNotFound {
            addAttribute(NSForegroundColorAttributeName, value: color, range: range)
        }
    }

    func setColorForAllOccuranceOfText(_ textToFind: String, with color: UIColor) {
        let inputLength = self.string.count
        let searchLength = textToFind.count
        var range = NSRange(location: 0, length: self.length)

        while (range.location != NSNotFound) {
            range = (self.string as NSString).range(of: textToFind, options: [], range: range)
            if (range.location != NSNotFound) {
                self.addAttribute(NSForegroundColorAttributeName, value: color, range: NSRange(location: range.location, length: searchLength))
                range = NSRange(location: range.location + range.length, length: inputLength - (range.location + range.length))
            }
        }
    }
}
Alden answered 19/7, 2018 at 10:36 Comment(0)
P
1

Using NSRegularExpression saves you from doing all the range calculations on by yourself. This example also will highlight two words instead of just one.

let text = "If you don't have a plan, you become part of somebody else's plan."
let toHighlight = ["plan", "you"]
let range = text.nsRange(from: text.startIndex ..< text.endIndex) // full text

let rangesToHighlight: [[NSRange]] = toHighlight.map { search in
    do {
        let regex = try NSRegularExpression(pattern: search, options: [])
        let matches: [NSTextCheckingResult] = regex.matches(in: text, options: [], range: range)
        return matches.map { $0.range } // get range from NSTextCheckingResult
    } catch {
        return [NSRange]()
    }
}

let yellow = UIColor.yellow
let attributedText = NSMutableAttributedString(string: text)

let flattenedRanges: [NSRange] = rangesToHighlight.joined()
flattenedRanges.forEach { // apply color to all ranges
    attributedText.addAttribute(NSForegroundColorAttributeName,
                                value: yellow,
                                range: $0)
}
Pebbly answered 27/9, 2017 at 13:30 Comment(0)
A
1

I’ve created an Extension for it in swift 4.2

extension NSMutableAttributedString {
// Adds attributes EVERY TIME the text to change appears
func addAttributes(_ attributes: [NSAttributedString.Key: NSObject], forText text: String) {
    var range = NSRange(location: 0, length: self.length)
    while (range.location != NSNotFound) {
        range = (self.string as NSString).range(of: text, options: [], range: range)
        if (range.location != NSNotFound) {
            self.addAttributes(attributes, range: NSRange(location: range.location, length: text.count))
            range = NSRange(location: range.location + range.length, length: self.string.count - (range.location + range.length))
        }
    }
}

Now you can call it like this:

let attributedString = NSMutableAttributedString(attributedString: textView.attributedText)
let myAttributes = [/* your attributes here */]
attributedString.addAttributes(myAttributes, forText: /* your text here */)
Associationism answered 14/7, 2018 at 4:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.