Strings are an integral part of almost every programming language, and Swift is no different. At its core, a string is a sequence of characters that can represent text. From user interfaces to data processing, strings are everywhere in software development. Having a deep understanding of strings is therefore foundational.

A Brief Overview of Strings in Swift

A string is nothing more than a sequence of characters. In Swift, that sequence of characters is encapsulated by the String struct. Swift's string implementation is fast and modern. It has no problem handling complex characters and sequences. Unlike other programming languages, a single character in Swift can represent more than a single byte or Unicode scalar. This depth and flexibility makes Swift's strings not only powerful but also intuitive to work with, especially for developers working with international or multilingual data. Don't worry about this for now. We take a closer look at this later.

Swift strings are value types, which means that when you create a string and assign it to a variable or pass it to a function, you are working with a unique copy of that string. It guarantees that strings are predictable and easy to work with, even as they are passed around in a multi-threaded environment.

Importance of Strings in Programming

As I wrote earlier, strings are everywhere in software development.

User Interaction: Every piece of text you see in your app, from button labels to user-generated content, is managed using strings. Strings are a vital component of the user experience of most apps.

Data Storage and Transmission: When data is stored on disk or transmitted over a network connection, it often takes the form of a string. Even binary data can be encoded as a string, for example, in the case of Base64 encoding.

Parsing and Processing: Strings allow your app to interpret and manipulate data. Whether it is reading a configuration file, parsing JSON returned from an API, or simply performing text-based calculations, strings make these operations possible.

Internationalization and Localization: Strings are at the heart of internationalization and localization. You can make your app accessible and user-friendly by using localized strings.

Understanding the Basics

Let's dive into the basics, starting with how to define a string.

Defining a String

In Swift, strings can be defined using double quotes (" "). Take a look at this simple example.

let helloWorld = "Hello, World!"

The constant helloWorld stores the string "Hello, World!". The compiler automatically infers the type of the helloWorld constant.

String Mutability

In Swift, you define the mutability of a string with the let (immutable) and var (mutable). A mutable string can be changed after it's been defined.

Immutable Strings (using let): Strings declared with the let keyword are constants and immutable. Their value cannot be changed after they've been set.

let greeting = "Hello"
greeting = "hi" // The compiler throws an error if you attempt to change a constant string.

The compiler throws an error if you attempt to modify an immutable string. This is true for any constant that stores a value type, not just strings.

The Definitive Guide to Working with Strings in Swift

Mutable Strings (using var): Strings declared with the var keyword are variables. Their value can be changed.

var name = "John"
name = "Bob"

Understanding the difference between variables (var) and constants (let) is essential. Watch of read Variables, Constants, and Types to learn more about this topic.

Empty Strings and Checking for Emptiness

A string can be empty in Swift. You create an empty string by initializing a String object or by using two double quotes.

let emptyString1 = ""
let emptyString2 = String()

Both emptyString1 and emptyString2 are empty strings. To check if a string is empty, you can use the computed isEmpty property of the String struct.

if emptyString1.isEmpty {
    print("The string is empty.")
} else {
    print("The string is not empty.")
}

String Interpolation in Swift

String interpolation is a flexible pattern to insert or embed values directly within a string. It is particularly useful for constructing dynamic strings without having to manually concatenate a collection of strings and values. Let me show you the basics of string interpolation.

Embedding Values into a String

In Swift, string interpolation is done using the \() syntax, a backslash followed by a pair of parentheses. You can place any value or expression inside the parentheses. Swift embeds the value or the result of the expression within the string. The value of the name variable is embedded into the greeting string.

let name = "John"
let greeting = "Hello, \(name)!"
print(greeting) // "Hello, John!"

You can embed more than just strings. You can interpolate numbers, booleans, and other data types.

let apples = 10
let oranges = 5
let fruitSummary = "I have \(apples) apples and \(oranges) oranges."
print(fruitSummary) // "I have 10 apples and 5 oranges."

Advanced Formatting Options

Swift has a few more tricks up its sleeve. You can embed more than simple values.

Formatting Numbers: You can control the number of decimal places or use other formatting styles.

import Foundation

let price = 3.14159265359
let formattedPrice = "The price is \(String(format: "%.2f", price))."
print(formattedPrice) // "The price is 3.14."

Embedding Expressions: Swift neatly evaluates expressions and embeds the result in the string.

let apples = 10
let oranges = 5
let fruitSummary = "I have \(apples + oranges) pieces of fruit."
print(fruitSummary) // "I have 10 pieces of fruit."

Using Multi-line Strings: Swift has a convenient syntax for long, multi-line strings. The multi-line string starts and ends with three double quotes (""").

let numberOfLines = 3
let text = """
This is a string that spans
\(numberOfLines) lines
without issues.
"""

print(text)

Accessing and Modifying Strings

As I wrote earlier, Swift's string implementation is powerful and flexible. This is thanks to Swift's understanding of the complexities of modern text, such as various encodings and multi-byte characters. This does mean that accessing and modifying strings may feel a bit odd at first. Let me show you what I mean by that.

Using String Indices

In Swift, you can't access the characters of a string using integer indices. That is because of the way Swift handles Unicode characters. You access the characters of a string using String.Index. Take a look at this example in which we access the start and end index.

let greeting = "Hello, World!"
let startIndex = greeting.startIndex
let endIndex = greeting.endIndex

We can then use the index to access the character that corresponds with the index. Note that firstChar is of type Character, not String. Remember that a string is a sequence of characters.

let firstChar = greeting[startIndex] // "H"

The String struct defines an intuitive API to access other characters of the string using indices.

let nextIndex = greeting.index(after: startIndex)
let fifthCharacter = greeting[greeting.index(startIndex, offsetBy: 4)] // "o"

Because Unicode characters have a variable width, it is important to always use String.Index to access the characters of a string, not integers.

Inserting and Removing Characters

As you may have guessed, we use String.Index to modify a variable string. In this example, we append an exclamation mark, a Character, to the end of the string.

var welcome = "Hello"
welcome.insert("!", at: welcome.endIndex) // "Hello!"

There are several APIs you can use to accomplish the same result as illustrated by the following example.

welcome.insert(contentsOf: " there", at: welcome.index(before: welcome.endIndex)) // "Hello there!"

The difference is that the insert(_:at:) method inserts a Character whereas the insert(contentsOf:at:) method inserts a collection of Characters.

To remove a character from a string, we invoke the remove(at:)method, passing in the index of the character to remove. The remove(at:) method returns the character that was removed.

welcome.remove(at: welcome.index(before: welcome.endIndex)) // "Hello there"

To remove a substring from a string, we specify the range of the substring and pass it to the removeSubrange(_:) method.

let range = welcome.index(welcome.endIndex, offsetBy: -6)..<welcome.endIndex
welcome.removeSubrange(range) // "Hello"

Substrings

I have to admit that working with substrings is initially a bit confusing. Let me explain why that is with an example.

import Foundation

let text = "The quick brown fox"
let start = text.startIndex
let end = text.index(start, offsetBy: 9)
let substring = text[start...end] // "The quick "

The type of the substring constant is String.SubSequence, which is a type alias for String.Substring. A String.Substring shares indices with the original sequence of characters, that is, the original string. This is efficient, but it can be confusing. String.Substring is designed to be temporary. You typically convert it back to a String.

import Foundation

let text = "The quick brown fox"
let start = text.startIndex
let end = text.index(start, offsetBy: 9)
let substring = text[start...end] // "The quick "
let newString = String(substring)

String API

The String struct defines a range of properties and methods to make your life as a developer easier. Let's take a look at some of the most used APIs.

Computed Properties

Counting Characters: The computed count property returns the number of characters of the string. Remember that a string is nothing more than a sequence or collection of Characters.

let hello = "Hello, World!"
let numberOfCharacters = hello.count // 13

Checking for Emptiness: We discussed the computed isEmpty property earlier. It returns true if the string contains no characters.

"".isEmpty // true

Accessing First and Last Character: The computed first property returns the first character of the string whereas the computed last property returns, you guessed it, the last character of the string. The type of these computed properties is Character? because an empty string doesn't have a first or last character.

let string = "Hello, World!"
string.first // "H"
string.last // "!"

"".first // nil
"".last // nil

Methods

Concatenating Strings and String Literals: Strings can be easily concatenated using the + operator or by using the += shorthand.

let string1 = "Hello"
let string2 = "World"
let string = string1 + " " + string2 // "Hello World"

Comparing and Querying Strings: There are several ways to compare strings. The == and != operators check for equality and inequality.

let string1 = "Hello"
let string2 = "World"

string1 == string2 // false
string1 != string2 // true

With the hasPrefix(_:) and hasSuffix(_:) methods, you can check if a string starts or ends with a given substring.

let string = "Hello World"

string.hasPrefix("Hello")
string.hasSuffix("World")

Case Operations: You can change the case of a string using the lowercased() and uppercased() methods. Note that these methods are not mutating. It returns a lowercased or uppercased copy of the string.

let string = "Hello World"

string.lowercased()
string.uppercased()

The computed capitalized property returns a string with each capitalized. You can also take the locale into account by using localizedCapitalized or capitalized(with:). The difference is that localizedCapitalized defaults to the current locale whereas capitalized(with:) takes a (optional) Locale as an argument.

import Foundation

let string = "hello world"

string.lowercased().capitalized
string.lowercased().localizedCapitalized
string.lowercased().capitalized(with: .current)

Searching within Strings

Searching within strings is a common operation, whether you're looking for a specific character, a substring, or trying to find the position of a particular sequence of characters. The String struct defines a set of useful APIs to make this straightforward without compromising accuracy and efficiency.

Finding a Character or Substring: Invoke the contains(_:) method of the String struct to check if a string contains a specific character or substring.

let string = "Swift rocks!"
string.contains("rock") // true

You can also use the contains(_:) method in combination with a collection of characters.

let string = "Swift rocks!"
let vowels: [Character] = ["a", "e", "i", "o", "u"]

string.contains { character in
    vowels.contains(character)
}

We can simplify this snippet like this.

let string = "Swift rocks!"
let vowels: [Character] = ["a", "e", "i", "o", "u"]

string.contains(where: vowels.contains)

Using the range(of:) Method: If you need to know the position of the character or substring, use the range(of:) method of the String struct. The range(of:) method returns a range that defines the position of the first occurrence of the substring. It returns nil if the character of substring isn't found within the string.

import Foundation

let string = "Swift rocks!"

if let range = string.range(of: "rock") {
    print("Found at \(range)")
} else {
    print("Not Found")
}

The returned range can be used to extract the substring or modify the string.

import Foundation

let string = "Swift rocks!"

if let range = string.range(of: "rock") {
    string[range] // "rock"
} else {
    print("Not Found")
}

As we discussed earlier, the indices returned by String's range(of:) method are of type String.Index. They're not integers. Remember that the use of String.Index guarantees accurate character access. This is essential given the complexities of Unicode and different character widths.

A String Is a Collection

One of Swift's strengths is its protocol-oriented design. The String struct conforms to several useful protocols that defines its behavior and interaction with other components of the language. This opens up a range of possibilities for working with text.

Collection Protocol

The String struct conforms to the Collection protocol, which means that a string behaves much like an ordered collection of its individual characters. This implies that you can use many of the same methods and properties that you might use with arrays, sets, and other collections. In the following example, we iterate over the characters of a string using a for-in loop.

let string = "Swift rocks!"

for character in string {
    print(character)
}

You already learned that you can access characters using indices, as you would with an array, but remember that the String struct uses the String.Index type.

let string = "Swift rocks!"

for character in string {
    print(character)
}

let firstCharacter = string[string.startIndex] // 'S'

Using Methods like map, filter, and reduce on Strings

Thanks to String's conformance to the Collection protocol, you can transform and manipulate methods you are already familiar with. Let's take a look at a few of those methods.

map: The map(_:) method transforms each character in the string and returns an array of the transformed elements.

let string = "Swift rocks!"

let asciiValues = string.map { $0.asciiValue! } // [83, 119, 105, 102, 116, 32, 114, 111, 99, 107, 115, 33]

You can also use the compactMap(_:) method if you prefer to avoid the exclamation mark.

let string = "Swift rocks!"

let asciiValues = string.compactMap { $0.asciiValue } // [83, 119, 105, 102, 116, 32, 114, 111, 99, 107, 115, 33]

filter: The filter(_:) method returns an array that only contains the characters that meet a given predicate. In this example, we extract the vowels from the string stored in the string constant.

let string = "Swift rocks!"

let vowels: [Character] = ["a", "e", "i", "o", "u"]
let vowelCharacters = string.filter { vowels.contains($0.lowercased()) }

reduce: The reduce(_:) method combines the characters in the string in a predefined manner. For example, you can use it to concatenate characters or calculate a cumulative value. In this example, we use the reduce(_:) method to uppercase the characters of the string.

let string = "Swift rocks!"

let combined = string.reduce("") { $0 + String($1).uppercased() } // "SWIFT ROCKS"

Strings and Unicode

Swift's string handling is deeply rooted in the Unicode standard and there is a very good reason for that. The approach the Swift team took several years ago guarantees that text is accurately and efficiently represented, regardless of language or script. Because this is such an important part of how Swift handles strings, I want to spend some time zooming in on the relation of Unicode and strings in Swift.

Understanding Unicode Scalar Values

Unicode scalar values are unique 21-bit numbers for each character, from U+0000 to U+D7FF and U+E000 to U+10FFFF. They are the building blocks of the Unicode standard. It guarantees that every character across different languages and scripts has a unique representation.

In Swift, you can represent a Unicode scalar value using the \u{...} escape sequence. Take a look at this example.

let heart = "\u{2665}" // ♥

Accessing and Iterating over Unicode Scalars

The String struct defines the computed unicodeScalars property. It is of type String.UnicodeScalarView and gives you access to the collection of Unicode scalar values (Unicode.Scalar objects) in the string. You can iterate over this collection to access each Unicode scalar.

let string = "hello world"

for scalar in string.unicodeScalars {
    print(scalar, scalar.value)
}

This is what the output in the console looks like. The computed value property returns the numeric representation of the Unicode scalar.

h 104
e 101
l 108
l 108
o 111
  32
w 119
o 111
r 114
l 108
d 100

Extended Grapheme Clusters

An extended grapheme cluster is a sequence of one or more Unicode scalars that, when combined, produce a single human-readable character. This concept is crucial because what appears as a single character can be composed of multiple Unicode scalars.

Let me illustrate this with an example to clear up any confusion. The character é can be represented as a single scalar (U+00E9) or as a combination of the letter e (U+0065) followed by the acute accent (U+0301).

In Swift, regardless of the internal representation, the following combinations are treated as a single character.

let string1: Character = "\u{E9}"
let string2: Character = "\u{65}\u{301}"

If we compare the strings stored in string1 and string2, they are equal.

let string1: Character = "\u{E9}"
let string2: Character = "\u{65}\u{301}"

string1 == string2 // true

I hope it's clear that Swift's string implementation guarantees that strings behave consistently and predictably, regardless of the underlying Unicode representations.