The Swift String API is hard to get used to. It has also changed over time as the Swift language and the standard library have developed. I first wrote this guide for Swift 2 and have since needed to update it for Swift 3, 4 and now Swift 5. So for my future reference and yours if you are struggling to make sense of it all here is my Swift String Cheat Sheet:
Last updated: Nov 10, 2022
Xcode Playground
You can get the latest version of the Xcode playground from my GitHub repository:
Version History
See the following posts for the main changes since I first wrote this guide for Swift 2:
- Creating Strings From Raw Text In Swift 5
- Character Properties In Swift 5
- Updating Strings for Swift 4.2
- Updating Strings For Swift 4
- Updating Strings For Swift 3
Initializing A String
There are an almost endless number of ways to create a String, using literals, conversions from other Swift types, Unicode, etc.
var emptyString = "" // Empty (Mutable) String
let stillEmpty = String() // Another empty String
let helloWorld = "Hello World!" // String literal
let a = String(true) // from boolean: "true"
let b: Character = "A" // Explicit type to create a Character
let c = String(b) // from character "A"
let d = String(3.14) // from Double "3.14"
let e = String(1000) // from Int "1000"
let f = "Result = \(d)" // Interpolation "Result = 3.14"
let g = "\u{2126}" // Unicode Ohm sign Ω
// New in Swift 4.2
let hex = String(254, radix: 16, uppercase: true) // "FE"
let octal = String(18, radix: 8) // "22"
Creating A String With Repeating Values
let h = String(repeating:"01", count:3) // 010101
Creating A String From A File
The file is in the playground Resources folder.
if let txtPath = Bundle.main.path(forResource: "lorem", ofType: "txt") {
do {
let lorem = try String(contentsOfFile: txtPath, encoding: .utf8)
} catch {
print("Something went wrong")
}
}
Multi-line String Literals (Swift 4)
Swift now allows you to create a multi-line String literals. You wrap the strings in triple double quotes ("""String"""
). You do not need to escape newlines and quotes within the string:
let verse = """
To be, or not to be - that is the question;
Whether 'tis nobler in the mind to suffer
The slings and arrows of outrageous fortune,
Or to take arms against a sea of troubles,
"""
You can control the leading white space with the indentation of the text relative to the closing """
. In the last example there is no leading whitespace in the final string literal. In this next example we indent the text by two spaces:
let indentedText = """
Hello, this text is indented by
two spaces from the closing quotes
"""
Source code with overly long string literals can be hard to read. To split long lines in the source use a \ to escape the new line.
let singleLongLine = """
This is a single longe line split \
over two lines by escaping the newline.
"""
Creating Strings From Raw Text (Swift 5)
When creating strings from raw text you can customise the delimiter and escape characters. Using the default delimiter (double-quote) and escape sequence (backslash) to create a String you might write:
let title = "Insert \"title\" here"
// Insert "title" here
Swift 5 allows you to pad the delimiter and escape sequence with one or more #
. We can write the previous example as follows:
let title2 = #"Insert "title" here"#
let title3 = ##"Insert "title" here"##
let title4 = ###"Insert "title" here"###
// Insert "title" here
Note that we don’t need to escape the double-quotes now as they are no longer a delimiter. If our raw text contains our chosen delimiter we can pad with an extra “#
“:
// raw text is "#Hello#"
// start delimiter is ##"
// end delimiter is "##
let regex1 = ##""#Hello#""## // "#Hello#"
If we pad the delimiters with one or more #
’s, we also need to pad the backslash escape sequence. For example, when interpolating a value:
let name = "Tom"
let greeting1 = "Hello \(name)" // Hello Tom
When padding with a single #
the escape sequence becomes \#
:
let greeting2 = #"Hello \#(name)"# // Hello Tom
Custom delimiters become useful when we want to preserve escaped raw text. For example, when creating a String from some JSON. Using a multi-line String literal seems like a good approach:
let json1 = """
{
"colors": ["red","green","blue"],
"label": "Insert \"title\" here"
}
"""
The multi-line string literal is convenient when the text contains quotes, but it introduces an error in this case. The problem is that the compiler strips the backslash escaping around “title” resulting in some invalid JSON:
{
"colors": ["red","green","blue"],
"label": "Insert "title" here"
}
If we use a custom delimiter with multi-line string literals we can keep the escape sequence in the raw text:
let json2 = #"""
{
"colors": ["red","green","blue"],
"label": "Insert \"title\" here"
}
"""#
The resulting String with the preserved raw text (note the backslash-escaped double-quotes aroung title):
{
"colors": ["red","green","blue"],
"label": "Insert \"title\" here"
}
Strings Are Value Types
Strings are value types (a Struct) that are copied when assigned or passed to a function. The copy is performed lazily on mutation.
var aString = "Hello"
var bString = aString
bString += " World!" // "Hello World!"
print("\(aString)") // "Hello\n"
Testing For Empty
let name = ""
name.isEmpty // true
let title = String()
title.isEmpty // true
Testing For Equality
Swift is Unicode correct so the equality operator ("==") checks for Unicode canonical equivalence. This means that two Strings that are composed from different Unicode scalars will be considered equal if they have the same linguistic meaning and appearance:
let spain = "España"
let tilde = "\u{303}"
let country = "Espan" + "\(tilde)" + "a"
if country == spain {
print("Matched!") // "Matched!\n"
}
Comparing For Order
if "aaa" < "bbb" {
print("aaa is less than bbb") // "aaa is less than bbb"
}
Testing For Suffix/Prefix
let line = "0001 Some test data here %%%%"
line.hasPrefix("0001") // true
line.hasSuffix("%%%%") // true
Converting To Upper/Lower Case
let mixedCase = "AbcDef"
let upper = mixedCase.uppercased() // "ABCDEF"
let lower = mixedCase.lowercased() // "abcdef"
Character Collections
In Swift 4 strings are back to being collections of characters. You can access different representations of the string through the appropriate collection view.
country.unicodeScalars // Unicode scalar 21-bit codes
country.utf16 // UTF-16 encoding
country.utf8 // UTF-8 encoding
Strings Are Collections Of Characters (Swift 4)
A String
is now a collection of characters by default so iterating over a String
or Substring
gives you each character in the `String:
// Swift 4
for character in country {
print(character)
}
To get the first or last character in a String
. The result is an optional returning nil if the String
is empty.
country.first // "E"
country.last // "a"
Random Element and Shuffle
Swift 4.2 allows you to get a random element from any collection. When used on a String
you get a random character or nil
if the String
is empty:
let suits = "♠︎♣︎♥︎♦︎"
suits.randomElement()
Iterate over shuffled String
for suit in suits.shuffled() {
print(suit)
}
Counting
Count is implemented for each of the collection views as it is dependent on the representation. The count
property of a String
is the character count.
// spain = España
spain.count // 6
spain.unicodeScalars.count // 6
spain.utf16.count // 6
spain.utf8.count // 7
Character Properties
Swift 5 adds convenient character properties. Definitions are from the Unicode standards.
Note: These proprerties operate on characters, not strings.
Testing For ASCII
Test if a character is ASCII (note that these properties operate on characters not strings):
let a = "A" as Character
let pi = "π" as Character
a.isASCII // true
pi.isASCII // false
The asciiValue
property is an optional integer that returns the ASCII value (or nil if the character is not ASCII):
a.asciiValue // Int? (65)
pi.asciiValue // nil
Testing For Whitespace and New Lines
The isWhitespace
property tests for spaces and other separator characters:
let tab = "\t" as Character
tab.isWhitespace // true
A new line character is also classed as whitespace. The isNewline
property tests more specifically for it (and other line separators):
let newline = "\n" as Character
newline.isWhitespace // true
newline.isNewline // true
Testing For Numbers
Test for numbers and whole numbers:
let five = "5" as Character
let half = "½" as Character
five.isNumber // true
half.isNumber // true
If the character is a whole number then wholeNumberValue
gives you the numeric value:
five.isWholeNumber // true
five.wholeNumberValue // Int? (5)
half.isWholeNumber // false
half.wholeNumberValue // nil
This also works for hexadecimal characters (upper or lower case):
let a = "A" as Character
a.isHexDigit // true
a.hexDigitValue // Int? (10)
Testing For Letters
Does the character represent an alphabetic letter:
a.isLetter // true
pi.isLetter // true (Greek alphabet)
let scream = "😱" as Character
scream.isLetter // false
Testing For Symbols
Test if a character is a symbol:
let smiley = "😀" as Character
smiley.isSymbol // true
smiley.isLetter // false
let plus = "+" as Character
plus.isSymbol // true
plus.isLetter // false
Test for a math symbol:
plus.isMathSymbol // true
smiley.isMathSymbol // false
Test for a currency symbol:
let dollar = "$" as Character
dollar.isCurrencySymbol // true
Punctuation
To test for punctuation marks:
let qmark = "?" as Character
qmark.isPunctuation // true
Upper And Lower Case
Properties to test for case and functions to convert the case:
let b = "b" as Character
let z = "Z" as Character
b.isLowercase // true
z.isUppercase // true
The functions to convert to upper or lower case return a String
as it can result in multiple characters:
b.uppercased() // B
pi.uppercased() // Π
z.lowercased() // z
let sharpS = "ß" as Character
sharpS.uppercased() // SS
The isCased
property is a strange one. It tests if the character changes when converted to upper or lower case:
z.isCased // true (z or Z)
b.isCased // true (b or B)
let half = "½" as Character
half.isCased // false (always ½)
Using Index To Traverse A Collection
Each of the collection views has an Index that you use to traverse the collection. This is maybe one of the big causes of pain when getting to grips with String. You cannot randomly access an element in a string using a subscript (e.g. string[5]).
Each collection has two instance properties you can use as subscripts to index into the collection:
startIndex
: the position of the first element if non-empty, else identical to endIndex.endIndex
: the position just “past the end” of the string.
When used directly with a String
or Substring
you get an index into the character view:
let hello = "hello"
let startIndex = hello.startIndex // 0
let endIndex = hello.endIndex // 5
hello[startIndex] // "h"
Note the choice for endIndex
means you cannot use it directly as a subscript as it is out of range.
Use index(after:)
and index(before:)
to move forward or backward from an index:
hello[hello.index(after: startIndex)] // "e"
hello[hello.index(before: endIndex)] // "o"
Use index(_:offsetBy:)
to move in arbitrary steps. A negative offset moves backwards:
hello[hello.index(startIndex, offsetBy: 1)] // "e"
hello[hello.index(endIndex, offsetBy: -4)] // "e"
You can also limit the offset to avoid an error when you run off the end of the index. The function index(_:offsetBy:limitedBy:)
returns an optional which will be nil
if you go too far:
if let someIndex = hello.index(startIndex,
offsetBy: 4, limitedBy: endIndex) {
hello[someIndex] // "o"
}
Using the utf16
view:
let cafe = "café"
let view = cafe.utf16
let utf16StartIndex = view.startIndex
let utf16EndIndex = view.endIndex
view[utf16StartIndex] // 99 - "c"
view[view.index(utf16StartIndex, offsetBy: 1)] // 97 - "a"
view[view.index(before: utf16EndIndex)] // 233 - "é"
The indices property returns a range for all elements in a String that can be useful for iterating through the collection:
for index in cafe.indices {
print(cafe[index])
}
You cannot use an index from one string to access a different string. You can convert an index to an integer value with the distance(from:to:)
method:
let word1 = "ABCDEF"
let word2 = "012345"
if let indexC = word1.firstIndex(of: "C") {
let distance = word1.distance(from: word1.startIndex, to: indexC) // 2
let digit = word2[word2.index(startIndex, offsetBy: distance)] // "2"
}
Finding Matches
The Sequence
and Collection
methods for finding the first and last element and index of an element that matched a predicate all work with String
:
Contains
Testing if a String
contains another String
let alphabet = "abcdefghijklmnopqrstuvwxyz"
alphabet.contains("jkl") // true
Finding First Or Last Match
To find the index of the first matching element (but note that the return value is an optional):
let k = alphabet.first { $0 > "j" } // "k"
if let matchedIndex = alphabet.firstIndex(of: "x") {
alphabet[matchedIndex] // "x"
}
let nomatchIndex = alphabet.firstIndex(of: "A") // nil
if let nextIndex = alphabet.firstIndex(where: { $0 > "j" }) {
alphabet[nextIndex] // "k"
}
Swift 4.2 also adds equivalent methods to find the last element:
let lastMatch = alphabet.last { $0 > "j" } // "z"
if let lastX = alphabet.lastIndex(of: "x") {
alphabet[lastX] // "x"
}
if let lastIndex = alphabet.lastIndex(where: { $0 > "j" }) {
alphabet[lastIndex] // "z"
}
Using A Range
To identify a range of elements in a string collection use a range. A range is just a start and end index:
let fqdn = "useyourloaf.com"
let tldEndIndex = fqdn.endIndex
let tldStartIndex = fqdn.index(tldEndIndex, offsetBy: -3)
let range = Range(uncheckedBounds: (lower: tldStartIndex, upper: tldEndIndex))
fqdn[range] // "com"
Creating A Range With ...
Or ..<
Operators
let endOfDomain = fqdn.index(fqdn.endIndex, offsetBy: -4)
let rangeOfDomain = fqdn.startIndex ..< endOfDomain
fqdn[rangeOfDomain] // useyourloaf
Returning The Range Of A Matching Substring
To return the range of a matching substring or nil
if not found:
if let rangeOfTLD = fqdn.range(of: "com") {
let tld = fqdn[rangeOfTLD] // "com"
}
Substrings
When you slice a string in Swift 4 you do not get back a String
you get a Substring
. A Substring
has most of the same methods as a String
(it conforms to StringProtocol
) which makes life easy.
A Substring
shares the storage of the original string. For this reason you should treat it a temporary object. From The Swift Programming Language (Swift 4):
substrings aren’t suitable for long-term storage – because they reuse the storage of the original string the entire original string must be kept in memory as long as any of its substrings are being used.
If you want to store it or pass it around convert it back to a String
.
In Swift 4 you slice a string into a substring using subscripting. The use of substring(from:)
, substring(to:)
and substring(with:)
are all deprecated.
To get a substring from an index up to the end of the string:
let template = "<<<Hello>>>"
let indexStartOfText = template.index(template.startIndex, offsetBy: 3) // 3
let indexEndOfText = template.index(template.endIndex, offsetBy: -3) // 8
// Swift 4
let substring1 = template[indexStartOfText...] // "Hello>>>"
// Swift 3 deprecated
// let substring1 = template.substring(from: indexStartOfText)
To get a substring from the start of the string up to an index:
// Swift 4
let substring2 = template[..<indexEndOfText] // "<<<Hello"
// Swift 3 deprecated
// let substring2 = template.substring(to: indexEndOfText)
To get a range within the String
// Swift 4
let substring3 = template[indexStartOfText..<indexEndOfText] // "Hello"
// Swift 3 deprecated
// let substring3 = template.substring(with: indexStartOfText..<indexEndOfText)
To directly get the range:
if let range3 = template.range(of: "Hello") {
template[range3] // "Hello"
}
Converting A Substring Back To A String
Use the String()
initializer to convert back to a String
.
let string1 = String(substring1)
Getting A Prefix Or Suffix
If you just need to drop/retrieve elements at the beginning or end of a String
. These all return a Substring
- use the String()
initializer if you need to convert back to a String
:
let digits = "0123456789"
let tail = digits.dropFirst() // "123456789"
let less = digits.dropFirst(3) // "3456789"
let head = digits.dropLast(3) // "0123456"
let prefix = digits.prefix(2) // "01"
let suffix = digits.suffix(2) // "89"
With Swift 4, prefer to use subscripting over the verbose prefix and suffix methods:
let index4 = digits.index(digits.startIndex, offsetBy: 4)
// The first of each of these examples is preferred
digits[...index4] // "01234"
digits.prefix(through: index4)
digits[..<index4] // "0123"
digits.prefix(upTo: index4)
digits[index4...] // "456789"
digits.suffix(from: index4)
Insert A Character At Index
var stars = "******"
stars.insert("X", at: stars.index(stars.startIndex, offsetBy: 3)) // "***X***"
Replace With Range
var stars = "***XYZ***"
if let xyzRange = stars.range(of: "XYZ") {
stars.replaceSubrange(xyzRange, with: "ABC") // "***ABC***"
}
Append
You concatenate strings with the +
operator or the append
method:
var message = "Welcome"
message += " Tim" // "Welcome Tim"
message.append("!!!") // "Welcome Tim!!!
Remove And Return Element At Index
This invalidates any indices you may have on the string.
var grades = "ABCDEF"
let ch = grades.remove(at: grades.startIndex) // "A"
print(grades) // "BCDEF"
Remove Range
Invalidates all indices.
var sequences = "ABA BBA ABC"
let lowBound = sequences.index(sequences.startIndex, offsetBy: 4)
let hiBound = sequences.index(sequences.endIndex, offsetBy: -4)
let midRange = lowBound ..< hiBound
sequences.removeSubrange(midRange) // "ABA ABC"
Bridging To NSString
String
is bridged to Objective-C as NSString
. If the Swift Standard Library does not have what you need import the Foundation framework to get access to methods defined by NSString.
Be aware that this is not a toll-free bridge so stick to the Swift Standard Library where possible.
Don’t forget to import Foundation
import Foundation
let welcome = "hello world!"
welcome.capitalized // "Hello World!"
Searching For A Substring
An example of using NSString
methods to perform a search for a substring:
let text = "123045780984"
// Find last occurance of "0"
if let rangeOfZero = text.range(of: "0", options: .backwards) {
// Get the characters after the last 0
let suffix = String(text.suffix(from: rangeOfZero.upperBound)) // "984"
print(suffix)
}
Further Reading
Swift evolution changes in Swift 5
Swift evolution changes in Swift 4.2:
Swift Evolution Changes in Swift 4
- SE-0183 Substring performance affordances
- SE-0182 String Newline Escaping
- SE-0180 String Index Overhaul
- SE-0178 Add unicodeScalars property to Character
- SE-0168 Multi-line String Literals
- SE-0163 String Revision: Collection Conformance, C Interop, Transcoding
Swift Evolution Changes in Swift 3