From the docs

Generic code enables you to write flexible, reusable functions and types that can work with any type, subject to requirements that you define. You can write code that avoids duplication and expresses its intent in a clear, abstracted manner.

I'm not trying to reinvent the wheel with this post, apple do a pretty great job of explaining what generics are and why they're useful, but still it can get a little tricky to know when to use them in everyday code.

You can think of generics as placeholders, we're giving our class, struct, protocol or function a type to use within code without it being defined and using that type as if it were concrete. Generics help us write type agnostic code while maintaining type safety, seems crazy right? To see an example you can look no further than the Array and Dictionary types in Swift (apple references the use in these types in the docs linked above).

As an example of how we can use generics in our code we're going to build a simple caching class that can store any type, it's going to have a simple add and fetch functionality.

// 1
class Cache<T> {
    // 2
    var cachedObjects: [String: T] = [String: T]()
    
    //3
    func retrieveValue(for key: String) -> T? {
        return cachedObjects[key]
    }
    
    func addObject(_ obj: T, for key: String) {
        cachedObjects[key] = obj
    }
}

let imageView = UIImageView(image: 
	UIImage(systemName: "magnifyingglass")!)
// 4
let cache = Cache<UIImageView>()
cache.addObject(imageView, for: "icon")

let retrievedImage = cache.retrieveValue(for: "icon")
retrievedImage

This isn't anything special, it's just a wrapper around a Dictionary (which also uses generics!), but it provides a useful example.

  1. Firstly we define our Cache object, and with it define a generic type with the syntax <T>, this is called a type parameter. You may have seen T used before in other examples, it's quite common when defining generics this can be anything, you could define it as <CachableObject> and use it in the same way, it's the triangular brackets that are important here.
  2. Next we define a dictionary using our generic type. It works like any other dictionary would, it's expecting the key to be of type String and the value to be of type T (defined later).
  3. retrieveValue(for key: String) -> T will take a String and return an object of type T  if it exists in the dictionary, otherwise nil.  The important thing to note here is that we're returning our generic type.
  4. This is where the magic happens. Cache<UIImageView>() is instantiating a Cache instance and we're passing in the type we want to use for T, so now in the implementation of Cache our type T has been replaced with UIImageView.

The cool thing here is thatUIImageView can be replaced by any other type that exists within our code. With these few lines we have a caching layer we could use within our project to store pretty much anything.

Type Constraints

We can constrain our generic types to other types; a type constraint is defined the same way as a protocol or subclass using :. Let's take a look at this, what if we want to extend our example to only take types of type UIView, well that's pretty simple...

class Cache<T: UIView> {
	// everything else remains the same
}

Now when creating a Cache instance, if we try and define our type as anything but a UIView subclass the compiler will throw an error.

let cache2 = Cache<String>()

defining a type constraint with a UIView gives us access to all the functionality of a UIView within our Cache object, now let's change our code to access our UIView object based on the UIView tag parameter.

class Cache<T: UIView> {
    
    // 1
    var cachedObjects: Set<T> = Set<T>()
    
    // 2
    func retrieveFirstView(with tag: Int) -> T? {
        return cachedObjects.filter({ $0.tag == tag }).first
    }
        
    func insertView(_ view: T) {
        cachedObjects.insert(view)
    }
}

let imageView = UIImageView(image: UIImage(systemName: "magnifyingglass")!)
imageView.tag = 99
let cache = Cache<UIImageView>()
cache.insertView(imageView)

let retrievedImage = cache.retrieveFirstView(with: 99)

Now we can just pass in the UIViews tag and the cache will return the first occurrence of a view with the provided tag if it exists, otherwise nil.

  1. We've updated cachedObjects to be a Set of our generic type.
  2. We've updated retrieveValue(for key: String) function to retrieveFirstView(with tag: Int) which uses the filter higher order function (docs here) to create an array of UIView objects with a specified tag, we then return the first object which defaults to nil if the set is empty.

This Cache isn't all that useful as it assumes you'll be giving each view a unique tag, it's also less efficient than our previous cache having a lookup speed of O(n) compared to the O(1) speed of the first, but I think this illustrates the use of type constraints pretty well.

Generic functions

We don't have to use generics for types like class and struct, we can also use generics in functions, this is declared in a similar way, the main difference here is that the generic type we pass in is only available within the scope of the function in which it is defined - let's take a look at an example.

func printDescription<T>(type: T) {
    print("\(type)")
}

printDescription(type: 100)
// 100

printDescription(type: "Hello")
// Hello

enum AwesomeEnum {case awesomeCase}

printDescription(type: AwesomeEnum.awesomeCase)
// awesomeCase

You see in the above example that it doesn't matter what type we pass as a parameter into this function. Note that unlike our Generic Type example we don't need to specify the type ahead of time, this is inferred when we pass in our variable, trying to define our type will result in an error.

printDescription<Int>(type: 100)
// Does not compile

Generic types in protocols

We can use Generic types in protocols, these are called associated types and are defined with the associatedtype keyword. Even though we define them differently, we can use them in the same way.

protocol Box {
    associatedtype Content
    
    mutating func fill(with contents: Content)
    func whatsInside()
}

We've defined a protocol here with the associatedtype Content, we have requirements for a subscriber of Box to be able to fill the box and to see what's inside, but just like in our other examples the type of Content is yet to be defined, let's extend this to a workable example.

protocol Box {
    associatedtype Content
    
    mutating func fill(with contents: Content)
    func whatsInside()
}

// 1
struct Item {
    let name: String
}

// 2
struct Present: Box {
    
    var contents: [Item]?
    
    // 3
    mutating func fill(with contents: [Item]) {
        self.contents = contents
    }
    
    // 4
    func whatsInside() {
        guard let contents = contents
            else {
                print("It's empty! 👻")
                return
        }
        
        contents.forEach({ print("\($0.name)") })
    }
}

// 5
let i1 = Item(name: "iPhone")
let i2 = Item(name: "Jumper")
let i3 = Item(name: "Socks")
let items = [i1, i2, i3]
var present = Present()
present.fill(with: items)

present.whatsInside()
// iPhone
// Jumper
// Socks
  1. We've defined a new type called Item with a single variable name
  2. We've defined another type Present which conforms to the Box protocol, note that we don't have any special requirements here, we subscribe to it like any other protocol.  It has a single variable contents which is an array of Item objects.
  3. This is the important part; here we implement the required function func fill(with contents:) and we pass in our type [Item], we're telling the compiler here to expect the associatedtype Content (defined in our protocol) to be of type Item.
  4. We provide an implementation for whatsInside() which simply prints the name variable.
  5. Here we define three items and pass them as the parameter to fill(with:), then call whatsInside to print the names of our items!

That's it on generics for now, I highly recommend you read the section in the Swift language guide on generics, they really are a powerful feature.

In the coming weeks we'll use what we've learned about generics to create a reusable, searchable list for use within our apps! stay tuned for that.

Thanks for reading! ✌️