4. Strings & characters¶
What & why¶
Swift's String is a full Unicode string, which makes it correct for every language and emoji โ and is the reason it works a little differently from strings in most languages. The big surprise: you can't index a string with an integer. This lesson explains why and what to do instead.
Literals, interpolation, multi-line¶
let name = "Ada"
let greeting = "Hello, \(name)! 2 + 2 = \(2 + 2)"
print(greeting) // Hello, Ada! 2 + 2 = 4
let quote = "She said \"hi\"\tand left.\n" // escapes: \n \t \\ \" \'
let poem = """
Roses are red,
Violets are blue.
Indentation up to the closing quotes is stripped.
"""
print(poem)
Raw strings (no escape processing) use #:
let path = #"C:\Users\ada\notes.txt"#
let pattern = #"\d+"#
print(#"Interpolate in raw strings with \#(name)"#)
Strings are value types¶
var a = "hello"
var b = a
b += " world"
print(a) // hello โ a was not affected
print(b) // hello world
Copying is cheap (Swift uses copy-on-write under the hood), and nobody can change your string out from under you.
Common operations¶
let s = "Swift is fun"
print(s.count) // 12 (number of Characters, not bytes)
print(s.isEmpty) // false
print(s.uppercased()) // SWIFT IS FUN
print(s.lowercased())
print(s.hasPrefix("Swift")) // true
print(s.hasSuffix("fun")) // true
print(s.contains("is")) // true
print(s + "!") // concatenation
print(String(repeating: "ab", count: 3)) // ababab
Splitting and joining:
let csv = "red,green,blue"
let parts = csv.split(separator: ",") // [Substring]: ["red", "green", "blue"]
print(parts.map(String.init)) // convert to [String]
print(["a", "b", "c"].joined(separator: " - ")) // a - b - c
Trimming and replacing (Foundation):
import Foundation
let raw = " padded \n"
print(raw.trimmingCharacters(in: .whitespacesAndNewlines)) // "padded"
print("a-b-c".replacingOccurrences(of: "-", with: "+")) // a+b+c
The standard library also has replacing(_:with:) (Swift 5.7+) which needs no Foundation:
Characters, and why there's no s[3]¶
A Character is one user-perceived character โ which may be several Unicode scalars:
let flag = "๐ฏ๐ต"
let accented = "รฉ" // might be 1 or 2 scalars depending on how it's typed
let family = "๐จโ๐ฉโ๐งโ๐ฆ"
print(flag.count, family.count) // 1 1
print(family.unicodeScalars.count) // 7
print(family.utf8.count) // 25 bytes
Because characters vary in size, finding the nth one means walking the string. So Swift uses indices, not integers:
let word = "Hello"
print(word[word.startIndex]) // H
print(word[word.index(word.startIndex, offsetBy: 4)]) // o
print(word[word.index(before: word.endIndex)]) // o (endIndex is one PAST the last char)
print(word.first!, word.last!) // H o
Looping over characters is simple and doesn't need indices:
If you need random access, convert once to an array: let chars = Array(word) and then chars[2].
Substrings¶
Slicing returns a Substring that shares memory with the original string:
let sentence = "the quick brown fox"
let firstWord = sentence.prefix(while: { $0 != " " }) // "the" (Substring)
let tail = sentence.dropFirst(4) // "quick brown fox"
let slice = sentence[sentence.startIndex..<sentence.index(sentence.startIndex, offsetBy: 3)]
let owned = String(firstWord) // make a standalone String when you store it
Rule of thumb: use Substring for quick work, then String(...) it before keeping it around (otherwise you keep the entire original string alive).
Comparing strings¶
print("apple" < "banana") // true โ lexicographic
print("Zebra" < "apple") // true โ uppercase sorts first
print("a" == "a") // true
print("รฉ" == "e\u{301}") // true โ canonically equivalent!
Equality uses canonical equivalence, so visually identical strings compare equal even if their scalars differ. For case-insensitive comparison:
import Foundation
print("Swift".caseInsensitiveCompare("SWIFT") == .orderedSame) // true
print("Swift".lowercased() == "SWIFT".lowercased()) // true (simple approach)
Converting to and from numbers¶
let n = 42
let text = String(n) // "42"
let back = Int(text) // Optional(42)
let hex = String(255, radix: 16) // "ff"
let value = Int("ff", radix: 16) // Optional(255)
Building strings efficiently¶
var out = ""
for i in 1...3 {
out += "Line \(i)\n" // fine for modest sizes
}
print(out, terminator: "")
+= on a var String appends in place (amortized) โ you don't need a StringBuilder.
Common mistakes¶
- Trying
s[0]. Not allowed; uses.first,s[s.startIndex], an index offset, orArray(s). - Assuming
countis O(1). It walks the string. UseisEmptyfor emptiness checks, nevercount == 0. - Using
endIndexas an element. It's one past the end โ subscripting it crashes. - Forgetting
String(substring)before storing a slice. - Thinking
countequals byte length. Use.utf8.countwhen an API wants bytes. - Unescaped backslashes in Windows paths โ use a raw string (
#"..."#).
Exercises¶
- Read a name and print it with the first letter capitalised and the rest lowercase (hint:
prefix(1),dropFirst()). - Write
isPalindrome(_ s: String) -> Boolthat ignores case and non-letters. - Count the vowels in a sentence using a
forloop over its characters. - Split
"2026-10-16"into year, month, dayInts. - Print the
count,unicodeScalars.count, andutf8.countof"cafรฉ ๐". Explain the three numbers. - Reverse a string two ways:
String(s.reversed()), and by building a new string in a loop. - Write a multi-line string literal containing a short ASCII-art box and print it.
Recap¶
Stringis Unicode-correct, a value type, and cheap to copy.- Interpolate with
\( ); use"""for multi-line and#"..."#for raw strings. - No integer subscripts โ use
String.Index,first/last,prefix/dropFirst, orArray(s). - Slices are
Substring; wrap withString(...)to keep them. - Prefer
isEmptyovercount == 0.