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.
- 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 seenT
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. - 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 typeT
(defined later). retrieveValue(for key: String) -> T
will take aString
and return an object of typeT
if it exists in the dictionary, otherwisenil
. The important thing to note here is that we're returning our generic type.- This is where the magic happens.
Cache<UIImageView>()
is instantiating aCache
instance and we're passing in the type we want to use forT
, so now in the implementation ofCache
our typeT
has been replaced withUIImageView
.
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 UIView
s tag and the cache will return the first occurrence of a view with the provided tag if it exists, otherwise nil.
- We've updated
cachedObjects
to be aSet
of our generic type. - We've updated
retrieveValue(for key: String)
function toretrieveFirstView(with tag: Int)
which uses thefilter
higher order function (docs here) to create an array ofUIView
objects with a specified tag, we then return the first object which defaults tonil
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
- We've defined a new type called
Item
with a single variablename
- We've defined another type
Present
which conforms to theBox
protocol, note that we don't have any special requirements here, we subscribe to it like any other protocol. It has a single variablecontents
which is an array ofItem
objects. - 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 theassociatedtype
Content
(defined in our protocol) to be of typeItem
. - We provide an implementation for
whatsInside()
which simply prints thename
variable. - Here we define three items and pass them as the parameter to
fill(with:)
, then callwhatsInside
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! ✌️