Strings

In Swift a String is a value type built from Unicode grapheme clusters, not raw bytes. That design makes text handling correct for every human language — at the cost of a few habits carried over from C or Java.

This lesson covers how to build strings, embed values, mutate them safely, and work with characters, indices, and substrings.

Creating Strings

Strings are created from literals, from interpolation, from other strings, or from a repeat count. The literal syntax is the one you will use constantly.

String Literals

A string literal is text surrounded by double quotes. Escape sequences let you embed characters that cannot be typed literally.

let plain = "Hello, Swift"        // ordinary literal
let escaped = "Line 1\nLine 2"    // \n is a newline escape
let quote = "She said \"hi\""     // \" embeds a double quote
let empty = ""                    // empty but valid

print(plain)                      // prints: Hello, Swift
print(empty.isEmpty)              // prints: true

Multiline Strings

Triple quotes create a multiline string. The closing delimiter's indentation decides how much leading whitespace is stripped from every line.

let poem = """
    Roses are red,
    violets are blue.
    """
print(poem)                       // prints two lines without the indentation

Raw Strings

Wrapping the literal in # disables escape sequences, which is convenient for regular expressions, Windows paths, and LaTeX.

let path = #"C:\Users\dev\file.txt"#   // backslashes stay literal

// #...# raw strings combine with interpolation by adding the same number of hashes
let name = "Ada"
let rawInterp = #"Hello \#(name)"#     // \#(...) interpolates inside a raw string
print(path)
print(rawInterp)                       // prints: Hello Ada

String Interpolation

Interpolation embeds the textual form of a value directly inside a literal, using the \( … ) syntax. It is more readable than manual concatenation and works with any type.

Embedding Values

Anything that can be printed can be interpolated — not just names, but whole expressions.

let name = "Ada"
let age = 36

let greeting = "Hello, \(name)! You are \(age)."   // names
let calc = "2 + 3 = \(2 + 3)"                      // an expression is allowed

print(greeting)                                    // prints: Hello, Ada! You are 36.
print(calc)                                        // prints: 2 + 3 = 5

Formatting Numbers

Interpolation uses a type's default description. When you need fixed decimals or zero padding, use String(format:) from Foundation.

import Foundation

let pi = 3.14159
print(String(format: "%.2f", pi))      // prints: 3.14 — two decimals

let n = 42
print(String(format: "%04d", n))       // prints: 0042 — zero-padded to width 4

Mutating & Concatenating

A String declared with var is mutable; one declared with let is frozen. Concatenation and append are mutating operations, so they require a variable.

Concatenation & Append

Use += to join two strings and append(_:) to add a character or string to the end of an existing value.

var message = "Hello"
message += ", world"          // concatenation with +=
message.append("!")           // append a Character or a String
print(message)                // prints: Hello, world!
print(message.count)          // prints: 13 — grapheme count, not byte count

var vs let Strings

Choosing let for text that never changes prevents accidental mutation and lets the compiler share the storage instead of copying it.

let fixed = "cannot change"
// fixed += "!"              // ❌ error: cannot use mutating member on immutable value

var growing = "can change"
growing += "!"                // fine
print(fixed, growing)         // prints: cannot change can change!

Characters & Unicode

Swift treats a string as a collection of characters, where one character is a full grapheme cluster. This is why count returns the number of things a human would call characters, not the number of bytes or scalars.

Characters

Iterating a string yields Character values. Build a Character from a single-element string literal, and convert between the two types explicitly.

let word = "Swift"
for ch in word {                    // each step yields a Character
    print(ch, terminator: " ")      // prints: S w i f t
}
print()                             // finish the line

let first = word.first ?? "?"       // Optional<Character> → "S"
let asString = String(first)        // Character → String
print(asString)                     // prints: S

Grapheme Clusters

Some visible characters are built from several Unicode scalars joined by combining marks. Swift counts the visible result as a single character, which avoids splitting text mid-symbol.

let flag = "🇷🇴"                    // two regional indicators, ONE character
let family = "👨‍👩‍👧"                 // several scalars joined, ONE character
let accent = "e\u{301}"             // "e" + combining acute → é

print(flag.count, family.count)     // prints: 1 1
print(accent.count)                 // prints: 1 — still one grapheme
print(accent == "é")                // prints: true — canonical equivalence

String Indices

Because characters have variable byte length, a string cannot be indexed by an integer. Use startIndex, endIndex, and index(_:offsetBy:) instead.

let text = "Swift"

let start = text.startIndex
let second = text.index(after: start)          // index of the 2nd character
print(text[second])                            // prints: w

let third = text.index(start, offsetBy: 2)
print(text[third])                             // prints: i

// A range gives a Substring — a view, not a copy
let slice = text[start..<third]
print(slice)                                   // prints: Sw
print(String(slice))                           // convert when you need a String

Common Operations

Day-to-day string work is mostly searching, splitting, and replacing. The standard library plus Foundation cover almost every case without writing a manual loop.

contains answers a yes/no question; range(of:) returns the location as an optional, because the text may not be present.

let sentence = "The quick brown fox"

print(sentence.contains("fox"))       // prints: true
print(sentence.hasPrefix("The"))      // prints: true
print(sentence.hasSuffix("fox"))      // prints: true

if let r = sentence.range(of: "quick") {          // Optional<Range>
    let offset = sentence.distance(from: sentence.startIndex, to: r.lowerBound)
    print("found at index \(offset)")             // prints: found at index 4
}

Splitting & Joining

split(separator:) returns an array of Substring values — lightweight views into the original text. joined(separator:) does the reverse.

let csv = "red,green,blue"
let parts = csv.split(separator: ",")          // [Substring]
print(parts.count, parts[0])                   // prints: 3 red

let joined = parts.joined(separator: " | ")    // Substring array → String
print(joined)                                  // prints: red | green | blue

Replacing Text

Replacement returns a new string, so assign the result. Foundation's variant handles whole words and supports options such as case-insensitivity.

import Foundation

var text = "I like apples"
text = text.replacingOccurrences(of: "apples", with: "oranges")
print(text)                                    // prints: I like oranges

Common Pitfalls

Counting in Loops

count is not stored — it walks the grapheme clusters each time. Calling it inside a loop turns linear work into quadratic work, so compute it once.

let sentence = "The quick brown fox"
let n = sentence.count              // compute ONCE, reuse below
for i in 0..<n {
    _ = i                           // pretend to use the index
}
print(n)                            // prints: 19

Index Out of Range

An index is only valid for the string it came from. Using one string's index on another compiles but traps at runtime, so derive indices fresh for each value.

let a = "abc"
let b = "xyz"
let idx = a.index(a.startIndex, offsetBy: 1)
print(a[idx])                       // prints: b
// print(b[idx])                    // ❌ trap: index belongs to a different string

You can now shape and search text confidently. Move on to Collections to store many values at once.