Writing

The hidden cost of string.count in Swift

4 Nov 2026, 3 min read

Here’s a quick self-test: you write a loop over n items, and inside it you call string.count. What’s the Big O?

If you said O(n), you’re thinking of count as a stored number. In a lot of languages it is. In Swift, on a String, it isn’t.

Why count is O(n) on a String

A Swift String is a collection of Characters, and a Character is an extended grapheme cluster: what a person sees as one character. “é” can be one Unicode scalar or two (an “e” plus a combining accent). A flag emoji is two scalars. A family emoji can be seven.

Because characters have different sizes, Swift can’t jump straight to the 500th character or know the total without walking the string. So String is a BidirectionalCollection, not a RandomAccessCollection, and count has to step through every character. That makes it O(m), where m is the length of the string.

What that does to your loop

// Looks harmless
var i = 0
while i < text.count {      // count is recalculated on every check
    // ...
    i += 1
}

text.count runs once per iteration, and each call walks the whole string. That’s O(m) work done m times: O(m²). The same thing happens if you index with text.index(text.startIndex, offsetBy: i) inside the loop, since each offset also walks from the start.

If you loop over n items and call count on a separate string of length m each time, it’s O(n · m).

The fixes

Hoist it. Compute the count once, outside the loop.

let length = text.count
for i in 0..<length { /* ... */ }

A for i in 0..<text.count loop is actually fine, because the range is built once. The trap is the while condition that re-evaluates.

Iterate directly. If you only need each character, skip the index math.

for character in text { /* ... */ }

Convert once when you need random access. For problems that compare positions, like two pointers or palindromes, pay O(m) once and get O(1) indexing after that.

let chars = Array(text)
var left = 0, right = chars.count - 1
while left < right, chars[left] == chars[right] {
    left += 1
    right -= 1
}

Check emptiness with isEmpty. text.isEmpty is O(1). text.count == 0 walks the whole string.

Takeaway

Big O isn’t only about the loops you write. It’s also about the cost of every call inside them. When I trace a problem by hand now, I ask what each call costs, not just how many times the loop runs.