4
votes

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?

1
I updated the Range<String.Index> <-> NSRange converter at stackoverflow.com/a/30404532/1187415 for Swift 3, does that help?Martin R
Awesome.. You rock.! So i found out that the problem was that NSMakeRange took arguments from and to, where as in swift 3, it takes arguments from and length. This caused the out of bounds exception.Wiingaard
I don't think that NSMakeRange is the problem, that did always take loc/len parameters.Martin R

1 Answers

7
votes

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
    }
}