Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift 3; Range 'Out of bounds'

I've just updated Xcode to 8.0 beta 2 and swift 3.0. After updating from swift 2.3 i'm getting a lot of errors.

I have a String-extension that's converting a Range in the "self"-string to a NSRange:

extension String {

    func NSRangeFromRange(_ range : Range<String.Index>) -> NSRange {

        let utf16view = self.utf16
        let from = String.UTF16View.Index(range.lowerBound, within: utf16view)
        let to = String.UTF16View.Index(range.upperBound, within: utf16view)

        print("to: \(to) from: \(from)")
        print(self.characters.count)

        return NSMakeRange(utf16view.startIndex.distance(to: from), from.distance(to: to))
    //    return NSMakeRange(0, 0)    // <-- removes exception
    }
}

When NSMakeRange is executed I'm getting a error:

Terminating app due to uncaught exception 'NSRangeException', reason: 'NSMutableRLEArray objectAtIndex:effectiveRange:: Out of bounds'

When I'm printing the to- and from-index's, I'm getting:

to: Index(_offset: 194) from: Index(_offset: 181)

The character-count of the String is 210, which seems about right.

So, I don't get why it's telling me that the index's are out of bounds, when they are less that the total count.

This line was working perfectly before I updated to swift 3. Back then it was looking like this:

return NSMakeRange(utf16view.startIndex.distanceTo(from), from.distanceTo(to))

The auto-converter didn't update the syntax from swift 2.3 to 3.0, I did that myselves..

Any clues?

like image 751
Wiingaard Avatar asked Mar 12 '23 18:03

Wiingaard


1 Answers

In Swift 3, "Collections move their index", see A New Model for Collections and Indices on Swift evolution.

In Swift 2.2, the advancedBy() and distanceTo() methods are called on the index:

let u16 = "12345".utf16
let i1 = u16.startIndex.advancedBy(1)
let i2 = u16.startIndex.advancedBy(3)
let d = i1.distanceTo(i2)
print(d) // 2

This methods still exist in Swift 3 but give unexpected results, at least on character collections:

let u16 = "12345".utf16
let i1 = u16.startIndex.advanced(by: 1)
let i2 = u16.startIndex.advanced(by: 3)
let d = i1.distance(to: i2)
print(d) // -2

The correct way is to use the index() and distance() methods of the collection itself:

let u16 = "12345".utf16
let i1 = u16.index(u16.startIndex, offsetBy: 1)
let i2 = u16.index(u16.startIndex, offsetBy: 3)
let d = u16.distance(from: i1, to: i2)
print(d) // 2

Applied to your problem, this is how you can convert a Range<String.Index> to the corresponding NSRange in Swift 3 (copied from https://stackoverflow.com/a/30404532/1187415):

extension String {
    func nsRange(from range: Range<String.Index>) -> NSRange {
        let utf16view = self.utf16
        let from = range.lowerBound.samePosition(in: utf16view)
        let to = range.upperBound.samePosition(in: utf16view)
        return NSMakeRange(utf16view.distance(from: utf16view.startIndex, to: from),
                              utf16view.distance(from: from, to: to))
    }
}

And for the sake of completeness, this is the opposite conversion

extension String {
    func range(from nsRange: NSRange) -> Range<String.Index>? {
        guard
            let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex),
            let to16 = utf16.index(from16, offsetBy: nsRange.length, limitedBy: utf16.endIndex),
            let from = String.Index(from16, within: self),
            let to = String.Index(to16, within: self)
        else { return nil }
        return from ..< to
    }
}
like image 165
Martin R Avatar answered Mar 15 '23 01:03

Martin R