3

I have the following code, in which I pass a string with of the type "Hello|r World|g" and the following function converts it into attributedString with "Hello" Being red in color and "World" being green in color. I used this as I am passing each of the strings in an array. The function only colors the text until it find a special character as shown in the condition during the end and then colors the text.

Code :

func formatAttributedString(string:String)->NSMutableAttributedString {
        var strCopy=string as NSString
        var color:UIColor=UIColor()
        var attributedString:NSMutableAttributedString!

        for var i:Int=0;i<strCopy.length-2;i++ {
            if (string[i] == "|") {
                println("|")
                var j:Int
                if string[i+1] == "r" {
                    color=UIColor(red: 249, green: 39, blue: 14, alpha: 1)
                    strCopy = strCopy.stringByReplacingOccurrencesOfString("|r", withString: "", options: NSStringCompareOptions.LiteralSearch, range: NSMakeRange(0, i + 2))
                    println("r")

                }
               else  if string[i+1] == "v" {
                    color=UIColor(red: 161, green: 153, blue: 249, alpha: 1)
                    strCopy = strCopy.stringByReplacingOccurrencesOfString("|v", withString: "", options: NSStringCompareOptions.LiteralSearch, range: NSMakeRange(0, i + 2))
                    println("v")

                }
                else if string[i+1] == "y" {
                    color=UIColor(red: 235, green: 223, blue: 145, alpha: 1)
                    strCopy = strCopy.stringByReplacingOccurrencesOfString("|y", withString: "", options: NSStringCompareOptions.LiteralSearch, range: NSMakeRange(0, i + 2))
                    println("y")
                }
                else if string[i+1] == "g" {
                    color=UIColor(red: 174, green: 227, blue: 79, alpha: 1)
                    strCopy = strCopy.stringByReplacingOccurrencesOfString("|y", withString: "", options: NSStringCompareOptions.LiteralSearch, range: NSMakeRange(0, i + 2))
                    println("g")
                }
                else if string[i+1] == "b" {
                    color=UIColor(red: 107, green: 224, blue: 240, alpha: 1)
                    strCopy = strCopy.stringByReplacingOccurrencesOfString("|b", withString: "", options: NSStringCompareOptions.LiteralSearch, range: NSMakeRange(0, i + 2))
                    println("b")
                }


                for j=i; j>=0;j-- {
                    if string[j] == " " || string[j] == "/" || string[j] == "." || string[j] == "\"" || string[j] == "\n" || string[j] == "<" || string[j] == "\t" || string[j] == "("{
                        println("/")
                        break

                    }
                }
                attributedString=NSMutableAttributedString(string: strCopy)
                attributedString.addAttribute("NSForegroundColorAttributeName", value: color, range: NSMakeRange(j, i-j))

            }
        }

I get the following error :

'NSMutableRLEArray objectAtIndex:effectiveRange:: Out of bounds'

As I have added printlns , | and r get printed. Please help,Thanks in advance.

It's not a duplicate of this question as | and r are getting printed.

Community
  • 1
  • 1
Dhruv Ramani
  • 2,633
  • 2
  • 22
  • 29
  • possible duplicate of [NSMutableAttributedStrings - objectAtIndex:effectiveRange:: Out of bounds](http://stackoverflow.com/questions/11571948/nsmutableattributedstrings-objectatindexeffectiverange-out-of-bounds) – Paresh Navadiya Apr 14 '15 at 10:53
  • You are mutating the string as you go, changing its length, so you need to reduce i after you call `stringByReplacingOccurrencesOfString` – Paulw11 Apr 14 '15 at 11:18
  • i--2 after you do the substitution. It would be ok except your for loop at the end starts using indexes which are 2 beyond the end of the string as you just shortened it. – Rory McKinnel Apr 14 '15 at 11:26

3 Answers3

1

My guess would be that error happens in this line:

attributedString.addAttribute("NSForegroundColorAttributeName", value: color, range: NSMakeRange(j, i-j))

Let's look at your string "Hello|r World|g" - it's 15 characters long. Let's look at the iteration of the outer for when it finds the second "|". strCopy is now "Hello World|g" - 13 characters long, i = 11. Program finds a "|" followed by "r" and changes strCopy into "Hello World|g" - 11 characters long and still i = 11.

After the "scan" in inner for, we end up with j = 7.

In this line you create a mutable string from "Hello World":

attributedString=NSMutableAttributedString(string: strCopy)

It has, just like strCopy itself, length equal 11.

Now, in the last line, you set attributes for characters in range NSRangeMake(7, 4), which means the last character to which it applies would be at index 11. Last index in this string is 10, that's why you get the crash.

EDIT: To avoid this crash you should add "i--;" after each line with stringByReplacingOccurencesOfString.

Another thing you should also do (which won't cause a crash, but will still make it malfunction is to change the inner for so that the loop line looks like this:

for j = i + (string.length - strCopy.length); j>=0; j-- {

johnyu
  • 2,152
  • 1
  • 15
  • 33
1

I have tried to meet your function signature with another implementation using Swift's anonymous tuple and higher ordered functions. I did this as an exercise for myself and at the end thought it best to share.

func formatAttributedString(string: String) -> NSMutableAttributedString {
    // create a mapping between the attribute token and the corresponding UIColor
    let colors = [
        "|r": UIColor(red: 249/255, green:  39/255, blue:  14/255, alpha: 1.0),
        "|v": UIColor(red: 161/255, green: 153/255, blue: 249/255, alpha: 1.0),
        "|y": UIColor(red: 235/255, green: 223/255, blue: 145/255, alpha: 1.0),
        "|g": UIColor(red: 174/255, green: 227/255, blue:  79/255, alpha: 1.0),
        "|b": UIColor(red: 107/255, green: 224/255, blue: 240/255, alpha: 1.0)]

    // split argument into an array of (String, UIColor) tuples

    // default the array to the entire argument string with a black color
    var substrings = [(string, UIColor.blackColor())]

    for (token, color) in colors {
        substrings = substrings.flatMap {
            var substrings = $0.0.componentsSeparatedByString(token)
            let tail = (substrings.removeLast(), $0.1)   // tuple for trailing string at old color
            var result = substrings.map{($0, color)}     // array of tuples for strings at new color
            result.append(tail)
            return result
        }
    }
    // because we default the original string to black, there may be an empty string tuple at the end
    substrings = substrings.filter{(string, _) in return !string.isEmpty}

    // convert array of (String, UIColor) tuples into a single attributed string
    var result = reduce(substrings, NSMutableAttributedString(string: "")) {
        var string = NSAttributedString(string: $1.0, attributes: [NSForegroundColorAttributeName: $1.1])
        $0.appendAttributedString(string)
        return $0
    }
    return result
}
Price Ringo
  • 3,424
  • 1
  • 19
  • 33
  • I get an error saying that '[String,UIColor] doesn't have a member names flatMap – Dhruv Ramani Apr 14 '15 at 17:38
  • flatmap is new to Swift 1.2. If you are using the latest version then the error indicates that substrings is a single tuple and not an array of tuples. flattop is definitely implemented for the Array type but not an anonymous tuple. FYI, this is running in my Playground. – Price Ringo Apr 14 '15 at 17:55
  • I just added an extension and added flatMap. I am debugging the other parts and if it works i'll accept the answer. Thanks!! – Dhruv Ramani Apr 14 '15 at 18:05
  • When I use this, all my attributes disappear and I get plain black text with default font. – Dhruv Ramani Apr 17 '15 at 14:22
  • Please remove the quotes from the attributes key in the reduce call. Should be ... attributes: [NSForegroundColorAttributeName: $1.1]... I will update the answer. – Price Ringo Apr 17 '15 at 17:25
0

Your algorithm seems to depend on i pointing to the last character processed. Whenever you encounter a |? pattern, you end up substituting the two special characters with "" effectively reducing the copy string size by 2.

The simplest way to solve your issue is to add i=i-2 after each call to strCopy.stringByReplacingOccurrencesOfString....

This keeps i correct for the code at the end of the function.

Ideally you might want to think about restructuring the function to move items from an original into a new version, adding color etc as you go. This would save all of the backward searching.

Rory McKinnel
  • 7,936
  • 2
  • 17
  • 28