147

I've been updating some of my old code and answers with Swift 3 but when I got to Swift Strings and Indexing it has been a pain to understand things.

Specifically I was trying the following:

let str = "Hello, playground"
let prefixRange = str.startIndex..<str.startIndex.advancedBy(5) // error

where the second line was giving me the following error

'advancedBy' is unavailable: To advance an index by n steps call 'index(_:offsetBy:)' on the CharacterView instance that produced the index.

I see that String has the following methods.

str.index(after: String.Index)
str.index(before: String.Index)
str.index(String.Index, offsetBy: String.IndexDistance)
str.index(String.Index, offsetBy: String.IndexDistance, limitedBy: String.Index)

These were really confusing me at first so I started playing around with them until I understood them. I am adding an answer below to show how they are used.

Tomerikoo
  • 18,379
  • 16
  • 47
  • 61
Suragch
  • 484,302
  • 314
  • 1,365
  • 1,393

5 Answers5

377

enter image description here

All of the following examples use

var str = "Hello, playground"

startIndex and endIndex

  • startIndex is the index of the first character
  • endIndex is the index after the last character.

Example

// character
str[str.startIndex] // H
str[str.endIndex]   // error: after last character

// range
let range = str.startIndex..<str.endIndex
str[range]  // "Hello, playground"

With Swift 4's one-sided ranges, the range can be simplified to one of the following forms.

let range = str.startIndex...
let range = ..<str.endIndex

I will use the full form in the follow examples for the sake of clarity, but for the sake of readability, you will probably want to use the one-sided ranges in your code.

after

As in: index(after: String.Index)

  • after refers to the index of the character directly after the given index.

Examples

// character
let index = str.index(after: str.startIndex)
str[index]  // "e"

// range
let range = str.index(after: str.startIndex)..<str.endIndex
str[range]  // "ello, playground"

before

As in: index(before: String.Index)

  • before refers to the index of the character directly before the given index.

Examples

// character
let index = str.index(before: str.endIndex)
str[index]  // d

// range
let range = str.startIndex..<str.index(before: str.endIndex)
str[range]  // Hello, playgroun

offsetBy

As in: index(String.Index, offsetBy: String.IndexDistance)

  • The offsetBy value can be positive or negative and starts from the given index. Although it is of the type String.IndexDistance, you can give it an Int.

Examples

// character
let index = str.index(str.startIndex, offsetBy: 7)
str[index]  // p

// range
let start = str.index(str.startIndex, offsetBy: 7)
let end = str.index(str.endIndex, offsetBy: -6)
let range = start..<end
str[range]  // play

limitedBy

As in: index(String.Index, offsetBy: String.IndexDistance, limitedBy: String.Index)

  • The limitedBy is useful for making sure that the offset does not cause the index to go out of bounds. It is a bounding index. Since it is possible for the offset to exceed the limit, this method returns an Optional. It returns nil if the index is out of bounds.

Example

// character
if let index = str.index(str.startIndex, offsetBy: 7, limitedBy: str.endIndex) {
    str[index]  // p
}

If the offset had been 77 instead of 7, then the if statement would have been skipped.

Why is String.Index needed?

It would be much easier to use an Int index for Strings. The reason that you have to create a new String.Index for every String is that Characters in Swift are not all the same length under the hood. A single Swift Character might be composed of one, two, or even more Unicode code points. Thus each unique String must calculate the indexes of its Characters.

It is possible to hide this complexity behind an Int index extension, but I am reluctant to do so. It is good to be reminded of what is actually happening.

mfaani
  • 33,269
  • 19
  • 164
  • 293
Suragch
  • 484,302
  • 314
  • 1,365
  • 1,393
  • 28
    Why would `startIndex` be anything else than 0? – Robo Robok Jul 03 '17 at 11:44
  • 32
    @RoboRobok: Because Swift works with Unicode characters, which are made of "grapheme clusters", Swift doesn't use integers to represent index locations. Let's say your first character is an `é`. It is actually made of the `e` plus a `\u{301}` Unicode representation. If you used an index of zero, you would get either the `e` or the accent ("grave") character, not the entire cluster that makes up the `é`. Using the `startIndex` ensures you'll get the entire grapheme cluster for any character. – leanne Aug 18 '17 at 16:16
  • 3
    In Swift 4.0 each Unicode characters are counted by 1. Eg: "‍".count // Now: 1, Before: 2 – selva Sep 29 '17 at 06:50
  • 3
    How does one construct a `String.Index` from an integer, other than building a dummy string and using the `.index` method on it? I don't know if I'm missing something, but the docs don't say anything. – sudo Oct 10 '17 at 20:43
  • 5
    @sudo, you have to be a little careful when constructing a `String.Index` with an integer because each Swift `Character` does not necessarily equal the same thing you mean with an integer. That said, you can pass an integer into the `offsetBy` parameter to create a `String.Index`. If you don't have a `String`, though, then you can't construct a `String.Index` (because Swift can only calculate the index if it knows what the previous characters in the string are). If you change the string then you must recalculate the index. You can't use the same `String.Index` on two different strings. – Suragch Oct 11 '17 at 02:15
  • @Suragch Thanks for the explanation. I've read up on how Swift handles characters, but it still seems odd that you can't create a `String.Index` to represent the i-th character in some string, whatever string you choose. For example, I'm running into this when handling server API responses that include integers I'm supposed to interpret as a character range. – sudo Oct 11 '17 at 06:01
  • 1
    @Suragch Please, how string.index(before: String.Index) internally works? Does it iterate backwards in the string per 2 bytes (UTF-16), trying to compose extended grapheme cluster with the next scalar, and stop when composing extended grapheme cluster is not possible? Then to return the index BEFORE the current Unicode char (string index). Or is there any smarter algorithm? – Michael Bernat Jan 08 '18 at 14:26
  • @MichaelBernat, I'm not sure. I couldn't find it in the [source code](https://github.com/apple/swift). Someone else can add the link if they can find it. – Suragch Jan 08 '18 at 16:03
  • I actually noted this link because of your good explanation. I cant figure out though how to use the lastIndex() because I actually want to get the last index + 1 (next character) which the after: is useful for. But I am having a hard time what the right combination is when using those 2. What i want is like in Java lastIndexOf("/") + 1 – chitgoks Aug 28 '22 at 23:38
1

If you want to hide the complexity of these functions, you can use this extension:

let str = " Hello"
str[0] //
extension String {
    subscript(_ index: Int) -> Character? {
        guard index >= 0, index < self.count else {
            return nil
        }

        return self[self.index(self.startIndex, offsetBy: index)]
    }
}
wattson
  • 81
  • 1
  • 8
0

I appreciate this question and all the info with it. I have something in mind that's kind of a question and an answer when it comes to String.Index.

I'm trying to see if there is an O(1) way to access a Substring (or Character) inside a String because string.index(startIndex, offsetBy: 1) is O(n) speed if you look at the definition of index function. Of course we can do something like:

let characterArray = Array(string)

then access any position in the characterArray however SPACE complexity of this is n = length of string, O(n) so it's kind of a waste of space.

I was looking at Swift.String documentation in Xcode and there is a frozen public struct called Index. We can initialize is as:

let index = String.Index(encodedOffset: 0)

Then simply access or print any index in our String object as such:

print(string[index])

Note: be careful not to go out of bounds`

This works and that's great but what is the run-time and space complexity of doing it this way? Is it any better?

C0D3
  • 6,440
  • 8
  • 43
  • 67
  • 1
    `String.Index(encodedOffset: 0)` gives you the index based on a UTF-16 encoding - so it works in O(1) on the assumption that your chars are all UTF-16. It was deprecated in Swift 5 and replaced with the more explicit (String.Index(utf16Offset: l, in: s)). In my opinion this should have been a question rather than an answer e.g. "What does String.Index(encodedOffset: 0) do and why its runtime is O(1)?" – tanz Sep 26 '21 at 06:52
-1
func change(string: inout String) {

    var character: Character = .normal

    enum Character {
        case space
        case newLine
        case normal
    }

    for i in stride(from: string.count - 1, through: 0, by: -1) {
        // first get index
        let index: String.Index?
        if i != 0 {
            index = string.index(after: string.index(string.startIndex, offsetBy: i - 1))
        } else {
            index = string.startIndex
        }

        if string[index!] == "\n" {

            if character != .normal {

                if character == .newLine {
                    string.remove(at: index!)
                } else if character == .space {
                    let number = string.index(after: string.index(string.startIndex, offsetBy: i))
                    if string[number] == " " {
                        string.remove(at: number)
                    }
                    character = .newLine
                }

            } else {
                character = .newLine
            }

        } else if string[index!] == " " {

            if character != .normal {

                string.remove(at: index!)

            } else {
                character = .space
            }

        } else {

            character = .normal

        }

    }

    // startIndex
    guard string.count > 0 else { return }
    if string[string.startIndex] == "\n" || string[string.startIndex] == " " {
        string.remove(at: string.startIndex)
    }

    // endIndex - here is a little more complicated!
    guard string.count > 0 else { return }
    let index = string.index(before: string.endIndex)
    if string[index] == "\n" || string[index] == " " {
        string.remove(at: index)
    }

}
xiawi
  • 1,772
  • 4
  • 19
  • 21
-6

Create a UITextView inside of a tableViewController. I used function: textViewDidChange and then checked for return-key-input. then if it detected return-key-input, delete the input of return key and dismiss keyboard.

func textViewDidChange(_ textView: UITextView) {
    tableView.beginUpdates()
    if textView.text.contains("\n"){
        textView.text.remove(at: textView.text.index(before: textView.text.endIndex))
        textView.resignFirstResponder()
    }
    tableView.endUpdates()
}
Jean-François Fabre
  • 137,073
  • 23
  • 153
  • 219