Chapters

Hide chapters

Swift Apprentice: Fundamentals

Second Edition · iOS 18 · Swift 6 · Xcode 16.2

Section III: Building Your Own Types

Section 3: 9 chapters
Show chapters Hide chapters

18. Generics
Written by Alexis Gallagher

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

The truth is, you already know about generics. Every time you use a Swift array, you’re using generics. This observation might give the impression that generics are about collections, but that impression is incorrect. In this chapter, you’ll learn the fundamentals of generics, giving you a solid foundation for understanding how to write generic code. Finally, you’ll loop back to look at generic types in the Swift standard library — arrays, dictionaries and optionals — using this new perspective.

Introducing Generics

To get started, consider how you might model pets and their keepers. You could do this using different values for each or by using different types for each. You’ll see that by using types, instead of values, the Swift type checker can reason about your code at compile time. Not only do you need to do less at runtime, but you can catch problems that would have gone under the radar had you just used values. Your code also runs faster.

Values Defined by Other Values

Suppose you’re running a pet shop that sells only dogs and cats and want to use Swift to model that business. To start, you define a type, PetKind, that can hold two possible values corresponding to the two kinds of pets that you sell:

enum PetKind {
  case cat
  case dog
}
struct KeeperKind {
  var keeperOf: PetKind
}
let catKeeper = KeeperKind(keeperOf: .cat)
let dogKeeper = KeeperKind(keeperOf: .dog)
enum EnumKeeperKind {
  case catKeeper
  case dogKeeper
}
.naw PiibopBigr(yiigezEs:.sav) NivGejm hawian BualisXixf yawaor .teq .acx HaehahKigq(jootacIv:.qit) alk.

Types Defined by Other Types

The model above fundamentally works by varying the values of types. Now consider another way to model the pet-to-keeper system — by varying the types themselves.

class Cat {}
class Dog {}
class KeeperOfCats {}
class KeeperOfDogs {}
Con Vaemiq (eg Cux...) Xef jlqit Juorax blkoh Yis inv. Xuurut (eq Vej...) ant.

Anatomy of Generic Types

Generics provide a mechanism for using one set of types to define a new set of types.

class Keeper<Animal> {}
Huf Xeafeh<Fed> Kib hqwuk Zaidar sxnam Raf Puosul<Vap>

var aCatKeeper = Keeper<Cat>()

Using Type Parameters

Usually, though, you’ll want to do something with type parameters.

class Cat {
  var name: String

  init(name: String) {
    self.name = name
  }
}

class Dog {
  var name: String

  init(name: String) {
    self.name = name
  }
}

class Keeper<Animal> {
  var name: String

  init(name: String) {
    self.name = name
  }
}
class Keeper<Animal> {
  var name: String
  var morningCare: Animal
  var afternoonCare: Animal

  init(name: String, morningCare: Animal, afternoonCare: Animal) {
    self.name = name
    self.morningCare = morningCare
    self.afternoonCare = afternoonCare
  }
}
let jason = Keeper(name: "Jason",
                   morningCare: Cat(name: "Whiskers"),
                   afternoonCare: Cat(name: "Sleepy"))

Generic Function Parameters

Functions can be generic as well. A function’s type parameter list comes after the function name. You can then use the generic parameters in the rest of the definition.

func swapped<T, U>(_ x: T, _ y: U) -> (U, T) {
  (y, x)
}

swapped(33, "Jay")  // returns ("Jay", 33)

Mini-Exercises

Type Constrained Generics

In your definition of Keeper, the identifier Animal serves as a type parameter, a named placeholder for some concrete type you supply later.

class Keeper<Animal: Pet> {
   /* definition body as before */
}
protocol Pet {
  var name: String { get }  // all pets respond to a name
}
extension Cat: Pet {}
extension Dog: Pet {}
func callForDinner<Animal>(_ pet: Animal) {
   // What can you write here?
}
func callForDinner<Animal: Pet>(_ pet: Animal) {
   print("Here \(pet.name)-\(pet.name)! Dinner time!")
}
func callForDinner(_ pet: some Pet) {
  print("Here \(pet.name)-\(pet.name)! Dinner time!")
}

Conditional Conformance

In addition to simple type constraints, you can define more complex type constraints using a generic where clause. You can use a where clause in defining functions, types, member functions, protocols, and extensions. It can constrain type parameters and associated types, letting you define rich relationships on top of generic types.

func callForDinner<Animal>(_ pet: Animal) where Animal: Pet {
  print("Here \(pet.name)-\(pet.name)! Dinner time!")
}
extension Array where Element: Cat {
  func meow() {
    forEach { print("\($0.name) says meow!") }
  }
}
protocol Meowable {
  func meow()
}

extension Cat: Meowable {
  func meow() {
    print("\(self.name) says meow!")
  }
}

extension Array: Meowable where Element: Meowable {
  func meow() {
    forEach { $0.meow() }
  }
}

Advanced Generic Parameters

Suppose you wish to write a function to find a lost animal. You start with an array of lost animals:

let lostPets: [any Pet] = [Cat(name: "Whiskers"), Dog(name: "Hachiko")]
/// Return a lost Cat.
func findLostCat(name: String, among lost: [any Pet]) -> Cat? {
  lost.lazy.compactMap {
    $0 as? Cat
  }.first {
    $0.name == name
  }
}
/// Return a lost Dog.
func findLostDog(name: String, among lost: [any Pet]) -> Dog? {
  lost.lazy.compactMap {
    $0 as? Dog
  }.first {
    $0.name == name
  }
}
func findLostPet(name: String, among lost: [any Pet]) -> (any Pet)? {
  lost.first { $0.name == name}
}
func findLost<Animal: Pet>(_ petType: Animal.Type, name: String, among lost: [any Pet]) -> (some Pet)? {
  lost.lazy.compactMap {
    $0 as? Animal
  }.first {
    $0.name == name
  }
}
findLost(Cat.self, name: "Whiskers", among:lostPets)
findLost(Dog.self, name: "Hachiko", among:lostPets)
func findLost<Animal: Pet>(_ petType: Animal.Type, name: String, among lost: [any Pet]) -> Animal? {
  lost.lazy.compactMap {
    $0 as? Animal
  }.first {
    $0.name == name
  }
}
findLost(Cat.self, name: "Whiskers", among:lostPets)?.meow()
// Whiskers says meow!

Arrays

While the original Keeper type illustrates that a generic type doesn’t need to store anything or use its type parameter, Array, one of the most common generic types, does both.

let animalAges: [Int] = [2,5,7,9]
let animalAges: Array<Int> = [2,5,7,9]

Dictionaries

Swift generics allow multiple type parameters, each with unique constraints. A Dictionary is a straightforward example of this.

struct Dictionary<Key: Hashable, Value> // etc..
let intNames: Dictionary<Int, String> = [42: "forty-two"]
let intNames2: [Int: String] = [42: "forty-two", 7: "seven"]
let intNames3 = [42: "forty-two", 7: "seven"]

Optionals

Finally, no discussion of generics would be complete without mentioning optionals. Optionals are enumerations, but they’re just another generic type, which you could have defined yourself.

enum OptionalDate {
  case none
  case some(Date)
}
enum OptionalString {
  case none
  case some(String)
}
struct FormResults {
  // other properties here
  var birthday: OptionalDate
  var lastName: OptionalString
}
enum Optional<Wrapped> {
  case none
  case some(Wrapped)
}
var birthdate: Optional<Date> = .none
if birthdate == .none {
  // no birthdate
}
var birthdate: Date? = nil
if birthdate == nil {
  // no birthdate
}

Challenge

Before moving on, here is a challenge to test your knowledge of generics. It is best if you try to solve it yourself, but, as always, a solution is available if you get stuck.

Challenge 1: Build a Collection

Consider the pet and keeper examples from earlier in the chapter:

class Cat {
  var name: String

  init(name: String) {
    self.name = name
  }
}

class Dog {
  var name: String

  init(name: String) {
    self.name = name
  }
}

class Keeper<Animal> {
  var name: String
  var morningCare: Animal
  var afternoonCare: Animal

  init(name: String, morningCare: Animal, afternoonCare: Animal) {
    self.name = name
    self.morningCare = morningCare
    self.afternoonCare = afternoonCare
  }
}
let christine = Keeper<Cat>(name: "Christine")

christine.lookAfter(someCat)
christine.lookAfter(anotherCat)

Key Points

  • Generics are everywhere in Swift: optionals, arrays, dictionaries, other collection structures, and most basic operators like + and ==.
  • Generics express systematic variation at the level of types via type parameters that range over possible concrete types.
  • Generics are like functions for the compiler. They are evaluated at compile-time and result in new types – specializations of the generic type.
  • A generic type is not a concrete type but more like a recipe, program, or template for defining new types.
  • Swift provides a rich system of type constraints, which lets you specify what types are allowed for various type parameters.
  • some Protocol refers to a concrete, generic type, while any Protocol refers to a concrete type in an existential box.
  • There are many ways to write generics with constraints, the most general being the generic where clause.
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.
© 2025 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now