Swift Subscripts Explained: Syntax, Examples, and Best Practices
Swift subscripts let you access values using square brackets instead of calling a method. You already use them with arrays and dictionaries, but Swift also lets you define your own subscripts inside custom types. Understanding how they work helps you build cleaner APIs, model data naturally, and avoid common indexing mistakes.
Quick answer: A Swift subscript is a special member you define with the subscript keyword so instances of a type can read or write values using square-bracket syntax like object[index]. Subscripts can have one or more input parameters and can be read-only or read-write.
Difficulty: Beginner
Helpful to know first: You’ll understand this better if you know basic Swift syntax, how functions and properties work, and how arrays and dictionaries are accessed.
1. What Is Subscripts?
A subscript is a special way to access data from a type using bracket syntax. Instead of writing a method call such as valueAt(index:), you can write type[index] or instance[key].
- Subscripts are declared with the subscript keyword.
- They can be added to classes, structures, and enumerations.
- They can accept one parameter or multiple parameters.
- They can return a value directly or allow both reading and writing through get and set.
- They are often used to make a custom type feel like a collection or lookup table.
You already use built-in subscripts all the time in Swift:
let numbers = [10, 20, 30]
let first = numbers[0]
let scores = ["Ana": 95, "Ben": 88]
let anaScore = scores["Ana"]In both cases, square brackets are using a subscript. Arrays and dictionaries provide their own implementations.
A common beginner question is whether a subscript is just a function with different syntax. It is similar in purpose, but subscripts are designed specifically for indexed or keyed access and make code feel more natural when your type behaves like a container.
2. Why Subscripts Matters
Subscripts matter because they improve readability and make custom types easier to use. If a type represents something that can be accessed by index, key, row and column, or another lookup value, subscript syntax often communicates that intent better than a regular method.
For example, imagine a matrix type. This is more natural:
let value = matrix[1, 2]than this:
let value = matrix.valueAt(row: 1, column: 2)Use subscripts when:
- Your type stores values that are naturally accessed by index or key.
- You want your API to feel similar to arrays, dictionaries, or matrices.
- Reading and optionally updating a contained value should look simple.
Avoid subscripts when:
- The operation is complex or has side effects.
- The meaning of the brackets would be unclear.
- A named method would better explain what the code does.
3. Basic Syntax or Core Idea
The basic syntax uses the subscript keyword followed by parameters and a return type. A read-only subscript returns a value. A read-write subscript uses get and set.
Read-only subscript
This example defines a type that stores the first seven multiples of a number and returns one using an index.
struct TimesTable {
let multiplier: Int
subscript(index: Int) -> Int {
multiplier * index
}
}The parameter list works a lot like a function parameter list, and the arrow shows the return type. Because there is only a single expression, Swift returns it directly.
Using it looks like this:
let tableOf3 = TimesTable(multiplier: 3)
let result = tableOf3[4]
print(result)This prints 12.
Read-write subscript
If you want callers to both read and assign values, provide get and set.
struct WeekTemperatures {
var values: [Int] = [21, 22, 20, 19, 23, 24, 22]
subscript(day: Int) -> Int {
get {
values[day]
}
set {
values[day] = newValue
}
}
}Here, newValue is the default name Swift gives to the incoming value in the setter.
You can use the subscript like a property with brackets:
var temps = WeekTemperatures()
print(temps[0])
temps[0] = 25
print(temps[0])This first reads the temperature for day 0, then updates it.
Subscript vs function
Functions are better when the action needs a descriptive name. Subscripts are better when access is naturally based on indices or keys.
struct Library {
var books: [String]
subscript(index: Int) -> String {
books[index]
}
func bookCount() -> Int {
books.count
}
}Reading a book by position fits bracket syntax, while asking for a count works well as a property or method depending on your design.
4. Step-by-Step Examples
Example 1: A simple custom lookup
This example maps student IDs to names. A dictionary already supports subscripts, but wrapping it inside a type can make the model clearer.
struct StudentDirectory {
private var students: [Int: String] = [
101: "Ava",
102: "Noah",
103: "Mia"
]
subscript(id: Int) -> String? {
students[id]
}
}The return type is optional because a requested ID may not exist. This mirrors dictionary behavior.
let directory = StudentDirectory()
print(directory[102] ?? "Not found")This prints the student name when found, or a fallback string otherwise.
Example 2: A read-write inventory type
Here the subscript lets you read and update stock values by product name.
struct Inventory {
private var stock: [String: Int] = [:]
subscript(product: String) -> Int {
get {
stock[product] ?? 0
}
set {
stock[product] = newValue
}
}
}This subscript returns 0 for products not yet in the dictionary, which may be more convenient than returning an optional.
var inventory = Inventory()
inventory["Keyboard"] = 12
inventory["Mouse"] = 20
print(inventory["Keyboard"])
print(inventory["Monitor"])This prints 12 and then 0.
Example 3: A multi-parameter subscript
Subscripts can take more than one parameter. A matrix is a classic example.
struct Grid {
let columns: Int
var values: [Int]
subscript(row: Int, column: Int) -> Int {
get {
values[(row * columns) + column]
}
set {
values[(row * columns) + column] = newValue
}
}
}The subscript converts a row and column into a single array index.
var grid = Grid(columns: 3, values: [1, 2, 3, 4, 5, 6])
print(grid[1, 2])
grid[0, 1] = 99
print(grid[0, 1])This accesses the value at row 1, column 2, then updates another position.
Example 4: Type subscripts with static data
Subscripts can also belong to the type itself by using static.
struct PlanetNames {
static let all = [
"Mercury", "Venus", "Earth", "Mars",
"Jupiter", "Saturn", "Uranus", "Neptune"
]
static subscript(index: Int) -> String {
all[index]
}
}You access this without creating an instance:
print(PlanetNames[2])This prints Earth.
5. Practical Use Cases
- A custom matrix type that reads and writes values using row and column positions.
- A wrapper around dictionary data where settings["theme"] is clearer than a custom lookup method.
- A game board that accesses squares as board[row, column].
- A cache type that exposes values by key while hiding internal storage details.
- A model object that translates human-friendly keys into underlying stored values.
- A type-level lookup table where static subscript syntax feels more natural than a separate method.
6. Common Mistakes
Mistake 1: Indexing outside valid bounds
Subscripts often forward to an array internally. If the index is too small or too large, your code can crash at runtime.
Problem: This code tries to read an array position that does not exist, which causes the runtime error Fatal error: Index out of range.
struct Scores {
var values = [90, 85, 88]
subscript(index: Int) -> Int {
values[index]
}
}
let scores = Scores()
print(scores[5])Fix: Validate the index before accessing storage, or return an optional for safer lookup.
struct SafeScores {
var values = [90, 85, 88]
subscript(index: Int) -> Int? {
guard values.indices.contains(index) else {
return nil
}
return values[index]
}
}
let safeScores = SafeScores()
print(safeScores[5] ?? 0)The corrected version works because it avoids reading invalid array positions.
Mistake 2: Trying to write through a read-only subscript
If a subscript only returns a value and does not define a setter, you cannot assign through it.
Problem: This code attempts to modify a value through a read-only subscript, so Swift reports an error because the subscript has no setter.
struct Catalog {
var items = ["Pen", "Pencil"]
subscript(index: Int) -> String {
items[index]
}
}
var catalog = Catalog()
catalog[0] = "Marker"Fix: Add a set block if writing should be supported.
struct Catalog {
var items = ["Pen", "Pencil"]
subscript(index: Int) -> String {
get {
items[index]
}
set {
items[index] = newValue
}
}
}
var catalog = Catalog()
catalog[0] = "Marker"The corrected version works because the subscript now explicitly supports assignment.
Mistake 3: Forgetting that dictionary-style lookups may return an optional
When a key might not exist, the returned value is often optional. Beginners sometimes treat it as a guaranteed non-optional value.
Problem: This code assumes the subscript always returns a String, but the lookup can fail, producing the compile-time error Value of optional type 'String?' must be unwrapped to a value of type 'String'.
struct AirportCodes {
var codes: [String: String] = [
"LHR": "London Heathrow",
"JFK": "John F. Kennedy International Airport"
]
subscript(code: String) -> String? {
codes[code]
}
}
let airports = AirportCodes()
let name: String = airports["LAX"]
print(name)Fix: Unwrap the optional with if let, guard let, or the nil-coalescing operator.
let airports = AirportCodes()
let name = airports["LAX"] ?? "Unknown airport"
print(name)The corrected version works because it safely handles the missing-key case.
7. Best Practices
Use subscripts only when bracket access feels natural
If the reader can easily understand what object[key] means, a subscript is a good choice. If the meaning is unclear, a named method is often better.
// Less preferred when meaning is vague
struct Report {
subscript(value: Int) -> String {
"Processed \(value)"
}
}
// Preferred when the operation deserves a name
struct Report {
func message(for value: Int) -> String {
"Processed \(value)"
}
}This makes the API clearer because the method name explains the operation.
Return an optional when a lookup may fail
When there might not be a matching result, returning an optional is often safer and more expressive than crashing.
struct UsernameLookup {
var users: [Int: String]
subscript(id: Int) -> String? {
users[id]
}
}This design forces callers to think about missing data instead of assuming success.
Validate indices in custom collection-like types
If your subscript wraps an internal array, add bounds checking when invalid indices are possible in normal use.
struct SafeBuffer {
var storage: [Int]
subscript(index: Int) -> Int? {
guard storage.indices.contains(index) else {
return nil
}
return storage[index]
}
}This reduces runtime crashes and gives callers a safer result to work with.
8. Limitations and Edge Cases
- A subscript does not need a name, so overusing them can make APIs less descriptive than named methods.
- If a subscript forwards directly to an array without checking bounds, invalid indices cause Fatal error: Index out of range.
- Dictionary-like subscripts often return optionals because a key may not exist.
- Read-only subscripts cannot be assigned to unless you add a setter.
- Subscripts can accept multiple parameters, but too many parameters can make the syntax hard to read.
- When a type is immutable because it was declared with let, you cannot assign through a writeable subscript on a value type such as a struct.
- Static subscripts work on the type itself, but they should still represent clear lookup behavior rather than arbitrary logic.
A common “not working” situation is trying to set a subscript value on a struct instance declared with let. Even if the subscript has a setter, the whole value is immutable, so the assignment is not allowed.
9. Practical Mini Project
Let’s build a small seat manager for a classroom. It uses a subscript so seats can be read and updated using square brackets. This is a realistic example because seating charts are naturally accessed by row and column.
struct Classroom {
let rows: Int
let columns: Int
private var seats: [String]
init(rows: Int, columns: Int) {
self.rows = rows
self.columns = columns
self.seats = Array(repeating: "Empty", count: rows * columns)
}
subscript(row: Int, column: Int) -> String? {
get {
guard row >= 0, row < rows,
column >= 0, column < columns else {
return nil
}
return seats[(row * columns) + column]
}
set {
guard row >= 0, row < rows,
column >= 0, column < columns,
let newValue = newValue else {
return
}
seats[(row * columns) + column] = newValue
}
}
}
var classroom = Classroom(rows: 2, columns: 3)
classroom[0, 0] = "Ava"
classroom[0, 1] = "Noah"
classroom[1, 2] = "Mia"
print(classroom[0, 0] ?? "Unavailable")
print(classroom[1, 2] ?? "Unavailable")
print(classroom[3, 0] ?? "Unavailable")This mini project shows several useful subscript ideas at once: multi-parameter access, safe bounds checking, optional return values, and write support through a setter.
10. Key Points
- A subscript lets you access values with square brackets instead of a named method.
- Swift types such as arrays and dictionaries already use subscripts heavily.
- You can define custom subscripts in structs, classes, and enums.
- Subscripts can be read-only or read-write using get and set.
- They can take one parameter or multiple parameters.
- Use subscripts when indexed or keyed access is natural and easy to understand.
- Be careful with array-backed subscripts, because invalid indices can crash your program.
- Optional return types are often the safest choice when a lookup may fail.
11. Practice Exercise
Create a Playlist struct that stores song titles in an array and supports subscript access by index.
- Add a read-write subscript that reads and updates a song title.
- Make the subscript return an optional String so invalid indexes do not crash.
- Create a playlist with three songs.
- Print the second song.
- Update the first song and print it again.
- Try reading an out-of-range index and print a fallback message.
Expected output: The program should print the second song title, then the updated first song title, then a fallback such as Song not found.
Hint: Use songs.indices.contains(index) inside the subscript.
struct Playlist {
var songs: [String]
subscript(index: Int) -> String? {
get {
guard songs.indices.contains(index) else {
return nil
}
return songs[index]
}
set {
guard songs.indices.contains(index),
let newValue = newValue else {
return
}
songs[index] = newValue
}
}
}
var playlist = Playlist(songs: ["Intro", "Midnight Drive", "Sunrise"])
print(playlist[1] ?? "Song not found")
playlist[0] = "Opening Track"
print(playlist[0] ?? "Song not found")
print(playlist[5] ?? "Song not found")12. Final Summary
Swift subscripts are a language feature that lets a type provide array-like or dictionary-like access using square brackets. They are especially useful when your type behaves like a container, lookup table, grid, or keyed storage object. You can make them read-only for simple lookup behavior or read-write with get and set when updates should be allowed.
In practice, the most important design decisions are whether bracket syntax feels natural, whether lookups should return optionals, and whether you need to guard against invalid indices. If you use subscripts thoughtfully, your Swift APIs become easier to read and more consistent with the standard library.
A good next step is to learn more about Swift properties and methods together, because subscripts sit conceptually between those two features and often work alongside both in well-designed types.