Skip to content

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:

print("a-b-c".replacing("-", with: "+"))

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:

for ch in word {
    print(ch, terminator: " ")
}
print()

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; use s.first, s[s.startIndex], an index offset, or Array(s).
  • Assuming count is O(1). It walks the string. Use isEmpty for emptiness checks, never count == 0.
  • Using endIndex as an element. It's one past the end โ€” subscripting it crashes.
  • Forgetting String(substring) before storing a slice.
  • Thinking count equals byte length. Use .utf8.count when an API wants bytes.
  • Unescaped backslashes in Windows paths โ€” use a raw string (#"..."#).

Exercises

  1. Read a name and print it with the first letter capitalised and the rest lowercase (hint: prefix(1), dropFirst()).
  2. Write isPalindrome(_ s: String) -> Bool that ignores case and non-letters.
  3. Count the vowels in a sentence using a for loop over its characters.
  4. Split "2026-10-16" into year, month, day Ints.
  5. Print the count, unicodeScalars.count, and utf8.count of "cafรฉ ๐Ÿ™‚". Explain the three numbers.
  6. Reverse a string two ways: String(s.reversed()), and by building a new string in a loop.
  7. Write a multi-line string literal containing a short ASCII-art box and print it.

Recap

  • String is 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, or Array(s).
  • Slices are Substring; wrap with String(...) to keep them.
  • Prefer isEmpty over count == 0.

Next: Booleans & conditionals โ†’