Every time you write array[0], dictionary["key"], or string[index], you’re calling a subscript. Subscripts are a shortcut for accessing parts of a value, and you can define them on your own types to give them the same natural square-bracket syntax. They’re also more powerful than they look: they can take several parameters, be generic, be static, and even power dynamic member lookup.
Subscripts in Swift
Defining a subscript
A subscript looks like a cross between a computed property and a method:
struct TimesTable {
let multiplier: Int
subscript(index: Int) -> Int {
multiplier * index
}
}
let threeTimes = TimesTable(multiplier: 3)
print(threeTimes[6]) // 18
This one is read-only, like a computed property with only a getter. Note one difference from functions: subscript parameters have no argument labels by default. That’s why you write threeTimes[6], not threeTimes[index: 6].
Read-write subscripts
Add get and set blocks to make a subscript assignable:
struct Grid {
let rows: Int
let columns: Int
private var values: [Double]
init(rows: Int, columns: Int) {
self.rows = rows
self.columns = columns
values = Array(repeating: 0, count: rows * columns)
}
func isValid(row: Int, column: Int) -> Bool {
row >= 0 && row < rows && column >= 0 && column < columns
}
subscript(row: Int, column: Int) -> Double {
get {
precondition(isValid(row: row, column: column), "Index out of range")
return values[row * columns + column]
}
set {
precondition(isValid(row: row, column: column), "Index out of range")
values[row * columns + column] = newValue
}
}
}
var matrix = Grid(rows: 2, columns: 2)
matrix[0, 1] = 1.5
matrix[1, 0] = 3.2
print(matrix[0, 1]) // 1.5
Three things to notice:
- Subscripts can take several parameters, so a two-dimensional grid reads naturally as
matrix[row, column]. - Setting through a subscript of a struct is a mutation, so
matrixmust be avar, just like with a mutating method. - The
preconditionmirrorsArray’s behavior: an invalid index is a programming error, so it stops the program instead of returning a wrong value. Storing a 2D grid in one flat array (row * columns + column) keeps the data contiguous, which is faster than an array of arrays.
Argument labels in subscripts
You can opt into a label by writing it explicitly, which helps when the meaning of the parameter isn’t obvious:
struct Library {
private var books = ["978-0-13-468599-1": "The Pragmatic Programmer"]
subscript(isbn isbn: String) -> String? {
books[isbn]
}
}
let library = Library()
print(library[isbn: "978-0-13-468599-1"] ?? "Not found")
Optional-returning subscripts
A very popular extension adds a non-crashing subscript to every collection. It returns nil instead of trapping on a bad index:
extension Collection {
subscript(safe index: Index) -> Element? {
indices.contains(index) ? self[index] : nil
}
}
let colors = ["red", "green", "blue"]
print(colors[safe: 1] ?? "none") // green
print(colors[safe: 10] ?? "none") // none
Use it where a missing element is a normal, expected situation, like reading an index that came from user input or a server. Don’t sprinkle it everywhere to avoid crashes: if an index should be valid, a crash in development is better than silently showing the wrong data.
Dictionary uses the same idea in its own API: its key subscript returns an optional, because a missing key is normal.
Overloading subscripts
A type can have several subscripts, as long as their parameter types or labels differ:
struct Spreadsheet {
private var cells: [String: String] = [:]
subscript(cell: String) -> String {
get { cells[cell, default: ""] }
set { cells[cell] = newValue }
}
subscript(column: Character, row: Int) -> String {
get { self["\(column)\(row)"] }
set { self["\(column)\(row)"] = newValue }
}
}
var sheet = Spreadsheet()
sheet["A1"] = "Name"
sheet["B", 1] = "Score"
print(sheet["B1"]) // Score
Array does this too: array[2] returns an element, while array[1...3] returns an ArraySlice.
Static subscripts
Since Swift 5.1, subscripts can belong to the type itself:
enum Theme {
private static let colors = ["primary": "#EE7956", "text": "#161C32"]
static subscript(name: String) -> String? {
colors[name]
}
}
print(Theme["primary"] ?? "")
Generic subscripts
Subscripts can be generic, which enables type-safe heterogeneous containers. Here’s a settings store where each key knows the type of its value:
struct SettingKey<Value> {
let name: String
}
struct SettingsStore {
private var storage: [String: Any] = [:]
subscript<Value>(key: SettingKey<Value>) -> Value? {
get { storage[key.name] as? Value }
set { storage[key.name] = newValue }
}
}
let fontSize = SettingKey<Int>(name: "fontSize")
let username = SettingKey<String>(name: "username")
var settings = SettingsStore()
settings[fontSize] = 17
settings[username] = "sam"
let size: Int? = settings[fontSize] // Int, no casting at the call site
print(size ?? 0, settings[username] ?? "")
The storage uses Any internally, but the public API never lets you put a String under fontSize.
Dynamic member lookup
@dynamicMemberLookup turns property syntax into a subscript call. Combined with key paths, it lets a type expose the properties of something it wraps, with full type checking:
struct Address {
var city: String
var zip: String
}
@dynamicMemberLookup
struct Customer {
var name: String
var address: Address
subscript<T>(dynamicMember keyPath: WritableKeyPath<Address, T>) -> T {
get { address[keyPath: keyPath] }
set { address[keyPath: keyPath] = newValue }
}
}
var customer = Customer(name: "Sam", address: Address(city: "Amman", zip: "11118"))
print(customer.city) // "Amman": really customer.address.city
customer.city = "Irbid"
print(customer.address.city) // Irbid
customer.city doesn’t exist as a property: the compiler rewrites it into customer[dynamicMember: \Address.city]. Because the parameter is a key path, a typo like customer.cty is still a compile error. SwiftUI’s Binding uses exactly this trick, which is why $model.name works.
Why String has no integer subscript
Coming from other languages, everyone tries this:
let word = "Swift"
let second = word[1] // ❌ cannot subscript String with an Int, use a String.Index instead.
Swift strings are collections of Characters, and a character can be made of several Unicode scalars (think of emoji with skin tones, or letters with combining accents). Finding the n-th character means walking the string from the start, which is O(n). An Int subscript would hide that cost behind syntax that looks O(1), so Swift makes you be explicit:
let thirdCharacter = word[word.index(word.startIndex, offsetBy: 2)]
print(thirdCharacter) // i
If you need fast random access to characters, convert once with Array(word) and index the array.
Under the hood
A subscript is compiled into accessor functions, just like a computed property: a getter, a setter, and, for types like Array and Dictionary, a special modify accessor. The modify accessor yields direct access to the element’s storage, so an in-place operation like:
var lists = [[1, 2], [3]]
lists[0].append(99) // appends in place, without copying the inner array
print(lists) // [[1, 2, 99], [3]]
modifies the inner array directly, instead of getting a copy, appending to it, and setting it back. Your own subscripts with a plain get/set use the get-then-set path, which is fine for most types, but means mutating through them copies the value out and back in.
Common pitfalls
- Hiding expensive work behind brackets. Readers assume subscripts are fast, like array access. If a lookup is O(n) or does I/O, a named method is more honest.
- Silent
nils. An optional subscript that returnsnilfor an invalid index can turn a real bug into a blank screen. - Mutating copies.
var row = grid.rows[0]; row[1] = 5changes the copy, not the grid. - Integer indexing into strings: use
String.Index, or convert to an array once if you need random access.
Interview questions
How do subscripts differ from methods? They’re called with square brackets, can be read-write like properties, and their parameters have no argument labels by default.
Why can’t you write string[2] in Swift?
Characters have variable size in memory, so reaching the n-th one is O(n). Swift uses String.Index to make that cost visible.
What does @dynamicMemberLookup do?
It turns value.member into a call to subscript(dynamicMember:). With a key path parameter, the lookup stays type-checked.
How does array[i].append(x) avoid copying the inner array?
Array’s subscript has a modify accessor that gives in-place access to the element, so the inner array is mutated directly.
Summary
- Subscripts give your types
[ ]access, read-only or read-write, with any number of parameters. - Labels are opt-in; subscripts can be overloaded, static, and generic.
- Optional-returning subscripts handle expected missing values; crashing ones catch programming errors.
@dynamicMemberLookupwith key paths forwards properties type-safely.Stringdeliberately has noIntsubscript, because character access is O(n).