Search code examples
swiftstringmacosline-numbers

How to get the full line range for a given character range in Swift?


Apple has an old example how to get the range of a whole line, given a particular character range:

Counting Lines of Text

In order to obtain the full line range of the first line, they call the following Objective-C function:

[string lineRangeForRange:NSMakeRange(0, 0)]

I tried to implement the same thing in Swift, but I can't make it work because the method signature has changed:

string.lineRange(for: NSRange(location: 0, length: 0))

throws a compiler error:

Argument type 'NSRange' (aka '_NSRange') does not conform to expected type 'RangeExpression'

RangeExpression is some weird protocol I haven't really understood in its entirety. However, I figured that Range<Bound> conforms to it, so I tried the following:

let range = NSRange(location: 0, length: 0)
textView.string.lineRange(for: Range<Int>(range)!)

This time I get another compiler error:

Generic parameter 'R' could not be inferred

I couldn't find any generic parameter R, neither in Range, nor in RangeExpression.

What's this all about and how can I make it work?


Solution

  • lineRange(for:) (essentially) expects a range of String.Index, not a range of integers. Here is a simple example:

    let string = "Line1\nLine2"
    
    // Find the full range of the line containing the first "1":
    if let range = string.range(of: "1") {
        let lineRange = string.lineRange(for: range)
        print(string[lineRange]) // Line1
    }
    

    The actual parameter is a generic parameter of type R : RangeExpression, R.Bound == String.Index, which means that you can also pass partial ranges like string.startIndex... or ..<string.endIndex.

    The Swift version of the Objective-C sample code

    NSString *string;
    unsigned numberOfLines, index, stringLength = [string length];
    for (index = 0, numberOfLines = 0; index < stringLength; numberOfLines++)
        index = NSMaxRange([string lineRangeForRange:NSMakeRange(index, 0)]);
    

    would be

    let string = "Line1\nLine2"
    
    var index = string.startIndex
    var numberOfLines = 0
    while index != string.endIndex {
        let range = string.lineRange(for: index..<index)
        numberOfLines += 1
        index = range.upperBound
    }
    
    print(numberOfLines)
    

    Here index..<index takes the role of NSMakeRange(index, 0).

    If the purpose is just to count (or enumerate) the total number of lines then an alternative is to use

    string.enumerateLines(invoking: { (line, _) in
        // ...
    })
    

    instead (compare How to split a string by new lines in Swift).