Chapters

Hide chapters

Swift Apprentice: Beyond the Basics

First Edition · iOS 16 · Swift 5.8 · Xcode 14.3

Section I: Beyond the Basics

Section 1: 13 chapters
Show chapters Hide chapters

2. Custom Operators, Subscripts & Keypaths
Written by Ehab Amer

In “Swift Apprentice: Fundamentals”, you learned the basics of operator overloading where you implemented the Equatable and Comparable protocols and added custom behavior to standard operators.

However, there are certain cases when overloading standard operators isn’t sufficient. This chapter will show you how to create custom operators from scratch and define your own subscripts. You’ll use subscripts as shortcuts for accessing the elements of custom types and provide keypaths as dynamic references to properties in your types.

Custom Operators

You declare an operator when you want to define custom behavior not covered by one of the standard operators. Think of exponentiation, for example. You could overload the multiplication operator since exponentiation means repeated multiplication. But this would be confusing. Operators should do only one type of operation, not two.

You’ll define an exponentiation operator, first only for a specific type, then extend it by making it generic. Before doing that, you need to know some theory about operator types. Time to dive in!

Types of Operators

There are three major types of operators: unary, binary and ternary.

  • Unary operators work with only one operand and are defined either as postfix if they appear after the operand or prefix if they appear before the operand. The logical-not operator (!) is a unary prefix operator, and the force unwrapping operator (also !) is a unary postfix one. You learned about them in “Swift Apprentice: Fundamentals - Chapter 3: Basic Control Flow” and “Chapter 6: Optionals”.
  • Binary operators work with two operands and are infix because they appear between them. All the arithmetic operators (+, -, *, /, %), comparison operators (==, !=, <, >, <=, >=) and most of the logical ones (&&, ||) are binary infix.

  • Ternary operators work with three operands. This operator (?:) is the only ternary operator in Swift, and you can’t make your own.

Your Own Operator

Let’s walk through the process of creating a new operator from scratch. You’ll create one for exponentiation. Since it’s a custom one, you can choose the name yourself. It’s usually best to stick to the characters /, =, -, +, !, *, %, <, >, &, |, ^ and ?, although many other Unicode characters are allowed. You may need to type it often, so the fewer keystrokes, the better. Since exponentiation is repeated multiplication under the hood, choosing something that reflects that is good. You’ll use ** since other languages also use this notation.

Now for the operator’s type. The ** operator works with two operands, an infix (binary) operator.

Here’s what the operator’s signature looks like:

infix operator **

Nothing fancy here: the operator’s name and type are bundled into one line of code with the operator keyword. As for the operator’s implementation, a naive one looks like this:

func **(base: Int, power: Int) -> Int {
  precondition(power >= 2)
  var result = base
  for _ in 2...power {
    result *= base
  }
  return result
}

The function takes two arguments of type Int and uses loops, ranges and wildcards to return the first argument raised to the power of the second one. Note the multiplication assignment operator in action.

Note: You use the wildcard pattern to discard the loop’s values. You’ll learn more about it and other pattern matching techniques in “Chapter 4: Pattern Matching”.

Now test your brand-new operator:

let base = 2
let exponent = 2
let result = base ** exponent

Compound Assignment Operator

Most built-in operators have a corresponding compound assignment version. Do the same for the exponentiation operator:

infix operator **=

func **=(lhs: inout Int, rhs: Int) {
  lhs = lhs ** rhs
}

The operator’s name is **= and its infix, just like the exponentiation operator created earlier. It has no return type and instead uses the inout keyword in front of the type of the operand you are modifying. You’ve already seen inout in action in “Swift Apprentice: Fundamentals - Chapter 5: Functions”. A function can mutate parameters marked with inout.

Here is how the operator works:

var number = 2
number **= exponent

The value number is modified by the **= operator in-place.

Mini-exercises

  1. Implement a custom multiplication operator for strings so that the following code works:
let baseString = "abc"
let times = 5
var multipliedString = baseString ** times
  1. Implement the corresponding multiplication assignment operator so that the following code runs without errors:
multipliedString **= times

Generic Operators

You want the exponentiation operator to work for all integer types. Update your operator implementations as follows:

func **<T: BinaryInteger>(base: T, power: Int) -> T {
  precondition(power >= 2)
  var result = base
  for _ in 2...power {
    result *= base
  }
  return result
}

func **=<T: BinaryInteger>(lhs: inout T, rhs: Int) {
  lhs = lhs ** rhs
}

Notice the BinaryInteger type constraint on the generic parameter. This constraint is required here as the *= operator used in the function body isn’t available on any type T. However, it’s available on all types that conform to the BinaryInteger protocol. The function’s body is the same as before since the generic operator does the same thing as its non-generic equivalent.

Your previous code should still work. Now that the operator is generic, test it with some types other than Int:

let unsignedBase: UInt = 2
let unsignedResult = unsignedBase ** exponent

let base8: Int8 = 2
let result8 = base8 ** exponent

let unsignedBase8: UInt8 = 2
let unsignedResult8 = unsignedBase8 ** exponent

let base16: Int16 = 2
let result16 = base16 ** exponent

let unsignedBase16: UInt16 = 2
let unsignedResult16 = unsignedBase16 ** exponent

let base32: Int32 = 2
let result32 = base32 ** exponent

let unsignedBase32: UInt32 = 2
let unsignedResult32 = unsignedBase32 ** exponent

let base64: Int64 = 2
let result64 = base64 ** exponent

let unsignedBase64: UInt64 = 2
let unsignedResult64 = unsignedBase64 ** exponent

The exponentiation operator now works for all integer types: Int, UInt, Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64 and UInt64.

Note: You can also use the pow(_ base: Double, _ exponent: Double) function from the operating system (Darwin on the Mac) for exponentiation, but it doesn’t work for all the above types. However, it does handle negative and fractional exponents and is O(log) instead of O(n) as in the naive implementation.

Precedence and Associativity

Your shiny new custom operator seems to work just fine, but if you use it in a complex expression, Swift won’t know what to do with it:

2 * 2 ** 3 ** 2 // Does not compile!

To understand this expression, Swift needs the following information about your operator:

  • Precedence: Should the multiplication be done before or after the exponentiation?
  • Associativity: Should the consecutive exponentiations be done left to right or right to left?

Without this information, the only way to get Swift to understand your code is to add parentheses.

2 * (2 ** (3 ** 2))

These parentheses tell Swift that the exponentiation happens before the multiplication and from right to left. If this is always the case, you can define this behavior using a precedence group.

Change your operator definition to the following:

precedencegroup ExponentiationPrecedence {
  associativity: right
  higherThan: MultiplicationPrecedence
}

infix operator **: ExponentiationPrecedence

Here, you’re creating a precedence group for your exponentiation operator, telling Swift it’s right-associative and has higher precedence than multiplication.

Swift will now understand your expression, even without parentheses:

2 * 2 ** 3 ** 2

Maybe that is a good thing and perhaps it’s not. You may choose to make associativity: none and force users to make things explicit with parenthesis.

That’s it for custom operators. Time for some fun with subscripts!

Subscripts

You’ve already used subscripts in “Swift Apprentice: Fundamentals - Chapter 7: Arrays, Dictionaries & Sets” to retrieve the elements of arrays and dictionaries. It’s high time you learned to create your very own subscripts. Think of them as overloading the [] operator to provide shortcuts for accessing elements of a collection, class, structure or enumeration.

The subscript syntax is as follows:

subscript(parameterList) -> ReturnType {
  get {
    // return someValue of ReturnType
  }
 
  set(newValue) {
    // set someValue of ReturnType to newValue
  }
}

As you can see, subscripts behave like functions and computed properties:

  • The subscript’s prototype looks like a function’s signature: It has a parameter list and a return type, but instead of the func keyword and the function’s name, you use the subscript keyword. Subscripts may have variadic parameters and can throw errors but can’t use inout or default parameters. You’ll learn more about errors in “Chapter 5: Error Handling”.

  • The subscript’s body looks like a computed property: It has a getter and a setter. The setter is optional, so the subscript can be either read-write or read-only. You can omit the setter’s newValue default parameter; its type is the same as the subscript’s return type. Only declare it if you want to change its name to something else.

Enough theory! Add a subscript to a Person class defined as follows:

class Person {
  let name: String
  let age: Int

  init(name: String, age: Int) {
    self.name = name
    self.age = age
  }
}

The Person class has two stored properties: name of type String and age of type Int, along with a designated initializer to kick things off.

Now suppose I want to create a version of myself right now, as follows:

let me = Person(name: "Ehab", age: 37)

It would be nice to access my characteristics with a subscript like this:

me["name"]
me["age"]
me["gender"]

If you run this, Xcode will output the following error:

Type "Person" has no subscripts members

Whenever you use the square brackets operator, you call a subscript method under the hood. Your class has no subscripts defined by default, so you have to declare them yourself.

Add the following code to the Person class with an extension like this:

extension Person {
  subscript(key: String) -> String? {
    switch key {
      case "name": return name
      case "age": return "\(age)"
      default: return nil
    }
  }
}

The subscript returns an optional string based on the key you provide: You return the key’s corresponding property value or nil if you don’t use a valid key. The switch must be exhaustive, so you need a default case.

The subscript is read-only, so its entire body is a getter — you don’t need to state that with the get keyword explicitly.

The above test code works now:

me["name"]
me["age"]
me["gender"]

And outputs:

Ehab
37
nil

Subscript Parameters

You don’t have to use names for the subscript’s parameters when calling the subscript, even if you don’t use underscores when declaring them.

Add external parameter names if you want to be more specific like this:

subscript(key key: String) -> String? {
  // original code
}

The parameter’s name appears in the subscript call now:

me[key: "name"]
me[key: "age"]
me[key: "gender"]

Use descriptive names for external parameters instead of their local counterparts if you want to add more context to the subscript:

subscript(property key: String) -> String? {
  // original code
}

me[property: "name"]
me[property: "age"]
me[property: "gender"]

Static Subscripts

You can define static subscripts for custom types in Swift:

class File {
  let name: String
  
  init(name: String) {
    self.name = name
  }
  
  // 1
  static subscript(key: String) -> String {
    switch key {
      case "path": return "custom path"
      default: return "default path"
    }
  }
}

// 2
File["path"]
File["PATH"]

The code works like this:

  1. Use static to create a static subscript that returns the default or custom path for File.
  2. Call the subscript on File instead of a File instance.

Dynamic Member Lookup

You use dynamic member lookup to provide arbitrary dot syntax to your type.

Consider the following:

// 1
@dynamicMemberLookup
class Instrument {
  let brand: String
  let year: Int
  private let details: [String: String]
  
  init(brand: String, year: Int, details: [String: String]) {
    self.brand = brand
    self.year = year
    self.details = details
  }
  
  // 2
  subscript(dynamicMember key: String) -> String {
    switch key {
      case "info": return "\(brand) made in \(year)."
      default: return details[key] ?? ""
    }
  }
}

// 3
let instrument = Instrument(brand: "Roland",
                            year: 2021, 
                            details: ["type": "acoustic", "pitch": "C"])
instrument.info 
instrument.pitch

Going through the above code step by step:

  1. Mark Instrument as @dynamicMemberLookup to enable dot syntax for its subscripts.
  2. Conform Instrument to @dynamicMemberLookup by implementing subscript(dynamicMember:).
  3. Call the previously implemented subscript using dot syntax. It returns either contents from details or more information about Instrument.

Using @dynamicMemberLookup here makes the contents of the details dictionary available as properties, which improves readability.

However, the compiler evaluates dynamic member calls at runtime, so you lose the usual compile-time safety. For example, this compiles without complaint:

guitar.dlfksdf  // Returns ""

You should use @dynamicMemberLookup judiciously as it can prevent the compiler from checking an entire class of errors that it could previously identify at compile time. You can compose this feature with keypaths that you’ll learn about in a moment to maintain type safety and prevent the above nonsense.

This code also works:

instrument.brand // "Roland"
instrument.year // 2021

A derived class inherits dynamic member lookup from its base one:

class Guitar: Instrument {}
let guitar = Guitar(brand: "Fender",
                    year: 2021, 
                    details: ["type": "electric", "pitch": "C"])
guitar.info

You use dot syntax to call the Guitar subscript since Guitar is an Instrument and Instrument implements @dynamicMemberLookup.

You may also use dynamic member lookup for class subscripts in Swift. They behave like static subscripts, and you can override them in subclasses:

// 1
@dynamicMemberLookup
class Folder {
  let name: String
  
  init(name: String) {
    self.name = name
  }
  
  // 2
  class subscript(dynamicMember key: String) -> String {
    switch key {
      case "path": return "custom path"
      default: return "default path"
    }
  }
}

// 3
Folder.path
Folder.PATH

Here’s what’s going on over here:

  1. Mark Folder as @dynamicMemberLookup to enable dot syntax for custom subscripts.
  2. Use class and dynamic member lookup to create a class subscript that returns the default or custom path for Folder.
  3. Call the subscript on Folder with dot syntax.

Subscripts are easy to use and implement and live somewhere between computed properties and methods. However, take care to use them sparingly. Unlike computed properties and methods, subscripts have no name to make their intentions clear. Subscripts are almost exclusively used to access a collection’s elements, so don’t confuse the readers of your code by using them for something unrelated and unintuitive!

Keypaths

Keypaths enable you to store references to properties. For example, this is how you model the tutorials on our website:

class Tutorial {
  let title: String
  let author: Person
  let details: (type: String, category: String)
  
  init(
    title: String,
    author: Person, 
    details: (type: String, category: String)
  ) {
    self.title = title
    self.author = author
    self.details = details
  }
}

let tutorial = Tutorial(title: "Object Oriented Programming in Swift", 
                        author: me, 
                        details: (type: "Swift", category: "iOS"))

Each tutorial has a certain title, author, type and category. Using keypaths, you can get the tutorial’s title like this:

let title = \Tutorial.title
let tutorialTitle = tutorial[keyPath: title]

You first use a backslash \ to create a keypath for the title property of the Tutorial class and then access its corresponding data with the keyPath(_:) subscript.

Keypaths can access properties several levels deep:

let authorName = \Tutorial.author.name
var tutorialAuthor = tutorial[keyPath: authorName]

You can also use keypaths for tuples in Swift:

let type = \Tutorial.details.type
let tutorialType = tutorial[keyPath: type]
let category = \Tutorial.details.category
let tutorialCategory = tutorial[keyPath: category]

Here you use keypaths to get type and category from details in tutorial.

Appending Keypaths

You can make new keypaths by appending to existing ones like this:

let authorPath = \Tutorial.author
let authorNamePath = authorPath.appending(path: \.name)
tutorialAuthor = tutorial[keyPath: authorNamePath]

You use the appending(path:) method to add a new keypath to the already defined authorPath and infer the keypath’s base type.

Setting Properties

Keypaths can change property values. Suppose you set up your very own jukebox to play your favorite song:

class Jukebox {
  var song: String
  
  init(song: String) {
    self.song = song
  }
}

let jukebox = Jukebox(song: "Nothing Else Matters")

You declare the song property as a variable because your best friend comes to visit and wants to listen to their favorite song instead:

let song = \Jukebox.song
jukebox[keyPath: song] = "Stairway to Heaven"

You use the song keypath to change the song for your friend, and everyone is happy now!

Keypath Member Lookup

You can use dynamic member lookup for keypaths:

// 1
struct Point {
  let x, y: Int
}

// 2
@dynamicMemberLookup
struct Circle {
  let center: Point
  let radius: Int
  
  // 3
  subscript(dynamicMember keyPath: KeyPath<Point, Int>) -> Int {
    center[keyPath: keyPath]
  }
}

// 4
let center = Point(x: 1, y: 2)
let circle = Circle(center: center, radius: 1)
circle.x
circle.y

Here’s what this code does:

  1. Declare a type Point with x and y coordinates.
  2. Annotate Circle with @dynamicMemberLookup to enable dot syntax for its subscripts.
  3. Create a subscript that uses keypaths to access center properties from Circle.
  4. Call center properties on circle using dynamic member lookup.

As you can see, using keypaths is more involved than using properties. With keypaths, accessing a property becomes a two-step process:

  1. First, you decide which property you need and create a keypath.
  2. Then, you pass this keypath to an instance using the keypath subscript to access the selected property.

By using dynamic member lookup with keypaths, you maintain type safety. In other words, you can access circle.x and circle.y, but circle.z and circle.sdlkfj don’t compile! This type safety makes these two language features a powerful combo.

Note: The SwiftUI framework uses dynamic member lookup with keypaths to automatically wrap your properties inside other types that manage the View state and redrawing updates. You may not even realize your type is being used this way because you can access all of its properties as you normally would.

Keypaths as Functions

You can use keypaths as functions if the function is a closure with only one parameter and the keypath’s returned type matches the returned type of the closure:

let anotherTutorial = Tutorial(title: "Encoding and Decoding in Swift", 
                               author: me, 
                               details: (type: "Swift", category: "iOS"))
let tutorials = [tutorial, anotherTutorial]
let titles = tutorials.map(\.title)

Here you use the title keypath to map tutorials to their titles.

Challenges

Before moving on, here are some challenges to test your custom operators, subscripts and keypaths knowledge. It’s best to try and solve them yourself, but solutions are available if you get stuck. These came with the download or are available at the printed book’s source code link listed in the introduction.

Challenge 1: Make It Compile

Modify the following subscript implementation so that it compiles in a playground:

extension Array {
  subscript(index: Int) -> (String, String)? {
    guard let value = self[index] as? Int else {return nil}
    switch (value >= 0, abs(value) % 2) {
      case (true, 0): return ("positive", "even")
      case (true, 1): return ("positive", "odd")
      case (false, 0): return ("negative", "even")
      case (false, 1): return ("negative", "odd")
      default: return nil
    }
  }
}

Challenge 2: Random Access String

Write a subscript that computes the character at a specific index in a string. Why is this considered harmful?

Challenge 3: Generic Exponentiation

Implement the exponentiation generic operator for float types so that the following code works:

let exponent = 2
let baseDouble = 2.0
var resultDouble = baseDouble ** exponent
let baseFloat: Float = 2.0
var resultFloat = baseFloat ** exponent
let baseCG: CGFloat = 2.0
var resultCG = baseCG ** exponent

Hint: Import the CoreGraphics framework to work with CGFloat.

Challenge 4: Generic Exponentiation Assignment

Implement the exponentiation assignment generic operator for float types so that the following code works:

resultDouble **= exponent
resultFloat **= exponent
resultCG **= exponent

Key Points

  1. Remember the custom operators mantra when creating brand new operators from scratch: With great power comes great responsibility. Make sure the additional cognitive overhead of a custom operator introduces pays for itself.
  2. Choose the appropriate type for custom operators: postfix, prefix or infix.
  3. Don’t forget to define any related operators, such as compound assignment operators, for custom operators.
  4. Use subscripts to overload the square brackets operator for classes, structures and enumerations.
  5. Use keypaths to create dynamic references to properties.
  6. Use dynamic member lookup to provide type-safe dot syntax access to properties.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.