Swift String Manipulation for Coding Interviews: The Reference Guide

June 19, 202610 min read
dsaalgorithmsinterview-prepswift
Swift String Manipulation for Coding Interviews: The Reference Guide
TL;DR
  • Swift strings have no integer indexing — convert with Array(s) to get a [Character] you can access in O(1) with plain integer indices.
  • String.Index jumps are O(n)s.index(startIndex, offsetBy: n) walks the UTF-8 buffer; use array conversion for two-pointer and sliding-window problems.
  • Substring shares the underlying buffer — slicing is O(1), but wrap with String() before storing in a dictionary, returning from a function, or passing to a helper.
  • s.count is O(n) in Swift — cache it as let n = s.count or you'll accidentally turn an O(n) loop into O(n²).
  • s.append() over s += "x"append is amortized O(1) via dynamic-array doubling; the += form reallocates the entire string on every call.
  • [key, default: 0] is the idiomatic frequency subscript — avoids nil-unwrapping and is safe for character counting across all interview patterns.
  • split drops empty subsequences by default — pass omittingEmptySubsequences: false when the problem requires preserving empty fields between delimiters.

Swift strings have a reputation for being painful in interviews. That reputation is earned. The language makes Unicode correctness non-negotiable, which means you cannot index into a string with an integer. That decision is philosophically correct and operationally annoying at 9am on a live coding call. Here's what you need without reaching for the docs mid-problem.


The Core Problem: No Integer Indexing

In Python you write s[0]. In Java you write s.charAt(0). In Swift you write this:

let s = "hello" let first = s[s.startIndex] // 'h'

This is not a quirk. Swift String uses a String.Index type because characters are not all the same width. An emoji takes more storage than an ASCII letter. If you could index with plain integers, index arithmetic would silently produce garbage on anything non-ASCII. Swift refuses to let you shoot yourself in the foot. You have to get creative about shooting yourself in other places instead.

The practical fix for most interview problems: convert to [Character] immediately if you need random access.

let s = "hello" let chars = Array(s) // [Character] let c = chars[2] // 'l', O(1)

Now you have O(1) index access and you can solve the problem with plain integer arithmetic. The conversion is O(n) time and O(n) space, but you were going to iterate the string anyway.


String.Index When You Can't Avoid It

For slicing a large string into overlapping windows, you want String.Index because slices share the underlying buffer (no copy). The API:

let s = "interview" let start = s.startIndex let third = s.index(start, offsetBy: 2) // 'i' let sub = s[start...third] // "int", a Substring

The methods you need:

MethodWhat it does
s.startIndexIndex of the first character
s.endIndexOne past the last character
s.index(after: i)Next index
s.index(before: i)Previous index
s.index(startIndex, offsetBy: n)Jump n positions, O(n)
s.index(startIndex, offsetBy: n, limitedBy: s.endIndex)Safe version, returns Optional

endIndex is a trap: s[s.endIndex] crashes at runtime. Use open ranges (i..<s.endIndex) not closed ranges, and use s.index(before: s.endIndex) to reach the last character. The compiler will not warn you. The crash will be instant and confusing.

HeckOverflow meme: "How do I A?" with top-voted answer explaining how to do B instead

Searching "Swift get character at index" in 2015 and discovering the String.Index rabbithole.


Substring Is Not String

Slicing a Swift string returns a Substring, not a String. They share the same buffer, so slicing is O(1), but they are different types. Function signatures that expect String reject Substring.

let s = "hello world" let sub = s.prefix(5) // Substring: "hello" let str = String(sub) // String: "hello", copied here

Convert to String explicitly when passing to a helper, storing in a dictionary, or returning from a function. The compiler will tell you when you forget, but the error message wastes time if you don't recognize it immediately. During a live interview, that 30-second pause while you figure out why your types don't match is 30 seconds you could spend solving the actual problem.


Characters, ASCII Values, and the Frequency Array

The classic frequency-array trick (26-element array for lowercase letters) needs character-to-index conversion. Swift does this via asciiValue:

let char: Character = "a" let val = char.asciiValue! // UInt8: 97

asciiValue returns UInt8?. For lowercase-only inputs, the forced unwrap is safe. The idiomatic helper:

func charIndex(_ c: Character) -> Int { Int(c.asciiValue! - Character("a").asciiValue!) }

You can also compare characters directly, which avoids the ASCII arithmetic entirely:

char >= "a" && char <= "z" // true for lowercase letters char.isLetter // true for any Unicode letter char.isNumber // true for digit characters char.isWhitespace // space, tab, newline

isLetter and isNumber are Unicode-aware, which matters for non-ASCII inputs. If the problem constrains input to lowercase ASCII, char >= "a" && char <= "z" is more explicit and avoids any ambiguity about what "letter" means internationally.

See the ASCII vs Unicode explainer for the underlying encoding difference.


Building Strings Without Exploding Complexity

String concatenation with += on a String literal creates a new string on every call. In a loop, that is O(n²). Two clean alternatives:

// Option A: build [Character], convert once at the end var chars: [Character] = [] for c in source { chars.append(c) } let result = String(chars) // one allocation // Option B: use .append() on a var String var s = "" s.append("h") s.append("i") // .append() is amortized O(1), same as dynamic array doubling

The trap is s = s + "x" inside a loop. That reads s, allocates a new string, copies s into it, appends x, and writes back. Every iteration. For a 1000-character string that's a million-character copy total. Use s.append("x") instead and save yourself an uncomfortable conversation about why your solution is O(n²).

For an explanation of why the O(n²) trap appears in every language, see What Is a StringBuilder.


The Methods You'll Actually Use

Checking and Searching

s.hasPrefix("abc") // Bool, O(n) s.hasSuffix("xyz") // Bool, O(n) s.contains("sub") // Bool, O(n) s.count // Int, O(n), cache it s.isEmpty // Bool, O(1), prefer over count == 0

count is O(n) in Swift because the runtime must walk the UTF-8 storage to count grapheme clusters. Cache it as a local constant if you reference it more than once. Calling s.count inside an O(n) loop is a classic interview gotcha that turns a clean solution into O(n²).

Transforming

s.lowercased() // String s.uppercased() // String s.trimmingCharacters(in: .whitespaces) // String s.replacingOccurrences(of: "a", with: "b") // String String(s.reversed()) // String

Splitting and Joining

let parts = s.split(separator: " ") // [Substring] let joined = parts.joined(separator: "-") // String // Closure variant for multiple separators let words = s.split { $0.isWhitespace } // [Substring] // With Foundation (avoid in pure-stdlib interviews) let parts2 = s.components(separatedBy: ", ") // [String]

split from the standard library drops empty subsequences by default. "a,,b".split(separator: ",") gives ["a", "b"], not ["a", "", "b"]. Pass omittingEmptySubsequences: false when empty fields matter.

Inserting and Removing

var s = "hello" let i = s.index(s.startIndex, offsetBy: 2) s.insert("X", at: i) // "heXllo" s.remove(at: i) // removes and returns 'X' s.removeAll { $0 == "l" } // "heo"

Prefix and Suffix Operations

s.prefix(3) // Substring: first 3 chars s.suffix(3) // Substring: last 3 chars s.dropFirst(2) // Substring: skip first 2 s.dropLast(2) // Substring: skip last 2

Swift String Manipulation Patterns for Interviews

Frequency Count

func frequency(_ s: String) -> [Character: Int] { var freq: [Character: Int] = [:] for c in s { freq[c, default: 0] += 1 } return freq }

The [key, default: value] subscript avoids the nil check and is the idiomatic Swift way.

Palindrome Check

func isPalindrome(_ s: String) -> Bool { let chars = Array(s) var left = 0 var right = chars.count - 1 while left < right { if chars[left] != chars[right] { return false } left += 1 right -= 1 } return true }

Convert first, then use integer indices. You could do this with String.Index but the code becomes harder to read and easier to get wrong under pressure.

Anagram Check

func isAnagram(_ s: String, _ t: String) -> Bool { guard s.count == t.count else { return false } var freq = [Character: Int]() for c in s { freq[c, default: 0] += 1 } for c in t { freq[c, default: 0] -= 1 if freq[c]! < 0 { return false } } return true }

Sliding Window on Characters

func maxVowels(_ s: String, _ k: Int) -> Int { let chars = Array(s) let vowels: Set<Character> = ["a", "e", "i", "o", "u"] var count = chars[0..<k].filter { vowels.contains($0) }.count var best = count for i in k..<chars.count { if vowels.contains(chars[i]) { count += 1 } if vowels.contains(chars[i - k]) { count -= 1 } if count > best { best = count } } return best }

Quick Reference Table

OperationSwiftComplexity
Lengths.countO(n), cache it
Character at indexArray(s)[i]O(n) to convert, O(1) after
Slices[i..<j]O(1), returns Substring
Iteratefor c in sO(n)
Empty checks.isEmptyO(1)
Containss.contains("x")O(n)
Starts/Endss.hasPrefix / hasSuffixO(n)
Lowercases.lowercased()O(n)
ReverseString(s.reversed())O(n)
Splits.split(separator: " ")O(n), returns [Substring]
Joinarr.joined(separator: "-")O(n)
Append chars.append(c)Amortized O(1)
ASCII valuec.asciiValue!O(1), returns UInt8
Is letterc.isLetterO(1), Unicode-aware
Is digitc.isNumberO(1), Unicode-aware
Frequencyfreq[c, default: 0] += 1O(1) per char

The Gotchas That Will Bite You

These are the ones that don't cause a compiler error. They just crash, or silently produce wrong output, right when your interviewer is watching.

s.endIndex is not a valid character position. Indexing into it crashes. Use s.index(before: s.endIndex) for the last character, or chars[chars.count - 1] after converting to an array.

count is not free. Calling s.count inside an O(n) loop makes it O(n²). Compute it once: let n = s.count.

split drops empty sequences by default. "a,,b".split(separator: ",") gives two elements, not three. Pass omittingEmptySubsequences: false to preserve them.

Substring causes type errors. Wrap with String(sub) when storing in a dictionary, passing to a helper, or returning from a function.

Character arithmetic doesn't exist. You cannot write char + 1. To get the next character by ASCII value: Character(UnicodeScalar(char.asciiValue! + 1)). In practice, use integer indices into a character array instead.

split returns [Substring], not [String]. Use .map { String($0) } if you need a typed [String].

Tom and Jerry meme: "I would do absolutely anything to ensure prod is stable except for testing my code properly"

Knowing every Swift string gotcha but still calling s.count inside a loop.

For a full catalogue of Swift footguns, the Swift coding interview gotchas guide covers the rest.


The Strategic Call: Array vs String.Index

You have two paths for almost every string problem.

Convert to [Character] early. You pay O(n) upfront and get integer indexing everywhere. Works for two-pointer, sliding window, frequency counting, reversals, anagram checks. This is the right default for almost everything an interview will throw at you.

Keep as String, use String.Index. You get O(1) slicing via Substring shared-buffer semantics. Worth it when you're doing many overlapping slices of a large string, like in rolling-hash or string search algorithms.

In an interview, say which one you're picking and why. "I'm converting to a character array because I need integer indexing for this two-pointer approach" takes three seconds and tells the interviewer you understand the tradeoff. Picking one silently and then fumbling a type error is what actually costs you. The code can be wrong and recoverable. The silence is what gets written into the feedback form.

That kind of narration is what separates a clean interview from a stressful one. SpaceComplexity runs voice-based mock interviews where you explain your approach as you code, and the feedback specifically covers whether your reasoning is coming through clearly. A few sessions of that fixes the silence habit faster than any amount of solo LeetCode.


Further Reading