0

I try to get the distance between a string's startIndex and another index, but get the following error in the first loop iteration. The code actually works with most string, but with some it crashes.

fatal error: cannot increment endIndex

let content = NSMutableAttributedString(string: rawContent, attributes: attrs)

var startIndex = content.string.characters.startIndex

while true {
    let searchRange = startIndex..<rawContent.characters.endIndex
    if let range = rawContent.rangeOfString("\n", range: searchRange) {
        let index = rawContent.characters.startIndex.distanceTo(range.startIndex)
        startIndex = range.startIndex.advancedBy(1)

        rawContent.replaceRange(range, with: "*")
        content.addAttribute(
            NSForegroundColorAttributeName,
            value: UIColor.redColor(),
            range: NSMakeRange(index, 1))
    }
    else {
        break
    }
}

content.replaceCharactersInRange(NSMakeRange(0, content.length), withString: rawContent)

content is NSMutableAttributedString and when the app crashes the variables have the following values:

range.startIndex: 164
content.string.characters.startIndex: 0
content.string.characters.endIndex: 437,
content.string.characters.count: 435

I don't understand why the error message says about increasing endIndex when I'm trying to calculate the distance from the startIndex and anotherIndex is less than the string length.

11
  • Are you sure the error is coming from those 4 lines of code? Commented Mar 30, 2016 at 18:07
  • The debugger stops in the last line. Commented Mar 30, 2016 at 18:09
  • I added all lines to the question. The debugger stops at line let index = ... in the first loop iteration. The code has worked fine so far, but no with some strings it started to crash. The strings contain normal text with some line feeds. Commented Mar 30, 2016 at 18:37
  • So basically you are going through this string and removing all \n characters and replacing them with space characters? Why not just use replaceOcurrencesOfString? Commented Mar 30, 2016 at 19:25
  • It's an attributed string and I add attributes to the replaced range. But that's not relevant as the code never goes to that point. Commented Mar 30, 2016 at 19:48

2 Answers 2

2

The cause for the error is that you are mixing Range<String.Index> and NSRange APIs. The first is counting in Characters and the second in UTF–16 code units. If you start with:

import Cocoa

let content = NSMutableAttributedString(string: "♥️♥️\n")

... then your code enters an infinite loop (this refers to @Tapani's original question and I haven't checked if this is still the case after his changes; the central problem remains the same though)! This is because:

NSString(string: "♥️♥️\n").length   // 5
"♥️♥️\n".characters.count           // 3

... so that you end up replacing (part of) the second heart with a space, leaving the new line in place, which in turn keeps you in the loop.

One way to avoid these problems is to do something along the lines of:

let content = NSMutableAttributedString(string: "♥️♥️\n")

let newLinesPattern = try! NSRegularExpression(pattern: "\\n", options: [])

let length = (content.string as NSString).length
let fullRange = NSMakeRange(0, length)

let matches = newLinesPattern.matchesInString(content.string, options: [], range: fullRange)

for match in matches.reverse() {
    content.replaceCharactersInRange(match.range, withAttributedString: NSAttributedString(string: " "))
}

content.string // "♥️♥️ "

If you copy and paste this into a playground and study the code (e.g. Alt-Click on method names to popup their API), you'll see that this code works exclusively with NSRange metrics. It is also much safer as we are looping through the matched ranges in reverse so you can replace them with substrings of different length. Moreover, the use of NSRegularExpression makes this a more general solution (plus you can store the patterns and reuse them elsewhere). Finally, replacing with NSAttributedString, in addition to replaceCharactersInRange being NSRange based, also gives you a greater control in the sense that you can either keep the existing attributes (get them from the range you are replacing) or add others...

Sign up to request clarification or add additional context in comments.

9 Comments

But that is not the cause for the crash as you can see from the indexes. My string does not have any characters, which are not in the range 0...255.
That can't be the fact since your characters.endIndex is 437 and characters.count is only 435, so one or two "rogue" characters must've sneaked in?
Also, how do you replace a range or add attributes to a range in NSAttributed string by using Swift String's indexes?
I would probably enumerate matches using NSRegularExpression in reverse and make my changes inside the enumeration block... do you need any help with that? I'd certainly start from scratch, though, since the code as it is now has numerous issues...
But still you need to be able to map the locatipns from Swift String indexes to NSAttributedString indexes for the attributes.
|
0

Your code worked fine for me, but you should be error checking. And since you are searching the entire content.string anyway why add the complexity of setting the search range?

Something like this would be simpler:

if let anotherIndex = content.string.rangeOfString("\n")
{
    let index = content.string.startIndex.distanceTo(anotherIndex.startIndex)
}

1 Comment

My code is actually a loop so it has the required checks and that's why it uses the range. But it crashes in the first round of the loop.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.