Master Swift Generics: A practical guide to code reuse

Generics are a great feature of Swift that allow you to generalize and reuse code in ways that would not be possible otherwise.

They are also a quite advanced feature and become a roadblock for many developers. The iOS SDK uses generic extensively, something that is especially true in SwiftUI.

In this article, I will show why generics exist and how to use them in your apps.



Architecting SwiftUI apps with MVC and MVVM

GET THE FREE BOOK NOW

Contents

You can’t understand generics until you find concrete examples that require them

The Swift standard library and all frameworks in Apple SDKs make heavy use of generics.

Luckily, the type inference performed by the Swift compiler often hides generic behind familiar code. You can go pretty far building your iOS apps without understanding generics or knowing they even exist.

But at some point, you are bound to bump into them.

As I will see you in this article, you won’t be able to reuse parts of your code unless you master Swift generics. Moreover, they are fundamental for protocol-oriented programming, especially to design a solid networking layer in your apps.

In my experience, my students often have a hard time wrapping their heads around Swift generics. The problem is that it’s hard to see why generics are useful until you reach the limitations of code without generics.

You usually need some experience as a developer to get to that point. In the end, beginners and even intermediate iOS developers can make apps without using generics if they accept some repetition in their code.

I have seen many articles explaining what generics are straight away, showing you how to use them from the start. The problem I find in that approach is that it only teaches you how generics work in the language.

What you don’t learn is how to recognize the instances in which your code can benefit from using them.

So, here, I will follow the opposite approach. I will start with simple, specific code and generalize it until we get to the point where we need generics.

Most of the code you write uses generics invisibly to make your life simpler

As an example, let’s build a couple of screens for a social network app like Facebook or Meetup, where people can join events. You can get the full Xcode project on GitHub. 

the mockups for the two similar screen in our generic sample app

As you can see, these two screens look pretty similar. While they display different information, their structure is the same.

Any non-trivial app you build will have code that you want to reuse for different purposes.

Sometimes, it is evident, like in the two app screens above. But that can happen in any part of your code, even the ones that don’t have a clear visual representation.

Let’s start defining some data we can use to fill our app’s interfaces. Such data often comes in JSON format from a remote API, so it’s a good practice to store some of it in .json files in your Xcode project for testing purposes.

// Events.json
[
	{
		"title": "Book club",
		"date": 1591454840,
		"participants": 12
	},
	...
]

// Participants.json
[
	{
		"name": "Quinten Kortum",
		"friends": 4,
		"joined": 1578250800
	},
	...
]

For conciseness, above, I only listed one item per file. You can find the full data in the Xcode project.

We now need two model types to represent events and people in our app and decode our JSON data using the Codable protocols.

struct Event: Decodable {
	let title: String
	let date: Date
	let participants: Int
}

struct Person: Decodable {
	let name: String
	let friends: Int
	let joined: Date
}

This is a first example of code that invisibly uses Swift generics. 

How Decodable uses generics is quite complicated, and you can dig you into it after reading this article if you want.

Here I only want to point out that there are generics involved, even if you don’t see them. The Codable protocols use generics to make something quite complicated, like the decoding and encoding of JSON data, look simple in your code.

Generalizing the return type of methods

We now want to decode our two JSON files and have data ready to test the interface we will build.

That’s pretty straightforward. All we have to do is read the data in each file and feed it to a JSONDecoder object.

struct TestData {
	static let events: [Event] = loadEvents()
	static let participants: [Person] = loadParticipants()
	
	static func loadEvents() -> [Event] {
		let url = Bundle.main.url(forResource: "Events", withExtension: "json")!
		let data = try! Data(contentsOf: url)
		let decoder = JSONDecoder()
		decoder.dateDecodingStrategy = .secondsSince1970
		return try! decoder.decode([Event].self, from: data)
	}
	
	static func loadParticipants() -> [Person] {
		let url = Bundle.main.url(forResource: "Participants", withExtension: "json")!
		let data = try! Data(contentsOf: url)
		let decoder = JSONDecoder()
		decoder.dateDecodingStrategy = .secondsSince1970
		return try! decoder.decode([Person].self, from: data)
	}
}

We have a lot of duplicated code, so we want to generalize and reuse it as much as possible.

It’s easy to generalize the first line of both methods. All we need is a String parameter for the name of each file. The following lines are identical, so we don’t have any problem there.

But the last line it’s not easy to generalize. There, we specify which model type to use, which is also the return type of our methods, i.e., [Event] and [Person].

But how can we generalize the return type of a method?

Swift offers the Any type that represents, well, any type. We can try and use that to have a single method that loads both events and participants, that’s not a great solution.

struct TestData {
	static let events: [Event] = readFile(named: "Events") as! [Event]
	static let participants: [Person] = readFile(named: "Participants") as! [Person]
	
	static func readFile(named name: String) -> [Any]  {
		let url = Bundle.main.url(forResource: name, withExtension: "json")!
		let data = try! Data(contentsOf: url)
		let decoder = JSONDecoder()
		decoder.dateDecodingStrategy = .secondsSince1970
		if let events = try? decoder.decode([Event].self, from: data) {
			return events
		} else if let participants = try? decoder.decode([Person].self, from: data) {
			return participants
		}
		return []
	}
}

Here, we try to decode the file using either model types. If we get some data back, then it was the correct type. Otherwise, we try the next one.

The code above works, but it has several problems.

  • We have to expand the conditional statement whenever we add a new decodable model type to our app. (If you are curious, that violates the Open-closed principle).
  • Using Any as a return type erases the type information. Every time we call the readFile(named:), we have to cast its result to our desired type using either as? or as!.
  • The compiler cannot help us. We can cast the result of readFile(named:) to any type whatsoever. If we make a mistake, we will discover it only when our app crashes (probably, in the hands of a user).

Using Swift generics to parametrize types in functions

We have finally got to the limits of traditional approaches.

In our readFile(named:), we don’t only need a parameter for a value, i.e., the name of the file to open, but also for a type, i.e., the return type of the method.

We want to be able to say whether the type returned by readFile(named:) is [Event] or [Person], like we do for normal parameters.

There is obviously a way of doing that. We are already passing a type parameter to the decode(_:from:) method of JSONDecoder.

And that’s, in fact, a generic. Again, we are using generics even if you don’t know what they are yet. But now, we see what generics are: they allow us to also use types as parameters and not just values.

In Swift, you declare generics immediately after the name of a function using angular brackets.

struct TestData {
	static let events: [Event] = readFile(named: "Events")
	static let participants: [Person] = readFile(named: "Participants")
	
	static func readFile<ModelType>(named name: String) -> [ModelType]  {
		let url = Bundle.main.url(forResource: name, withExtension: "json")!
		let data = try! Data(contentsOf: url)
		let decoder = JSONDecoder()
		decoder.dateDecodingStrategy = .secondsSince1970
		return try! decoder.decode([ModelType].self, from: data)
	}
}

(This code still does not compile. We will see why in a moment.)

Some developers name their generics using single letters, like T, U, etc., but I find that too hard to read.

Try to use a meaningful name whenever possible, which is also the approach of the Swift standard library. In this case, this method deals with model types, so ModelType is a better name than just M.

Once you declare a generic in a method, you can use it as:

  • the return type (our case);
  • the type of any parameter;
  • the type of any local constant/variable;
  • the parameter of another generic function.

Our ModelType generic acts as a placeholder for a type that we don’t know. That type is decided only when we use the method. 

You can see that in the two calls to readFile(named:). There, we don’t need type casting anymore. The events and participants static properties of the TestData structure have an explicit type. Thanks to type inference, the Swift compiler can determine what type to use in each call.

That allows the compiler to make all the necessary checks and warn you if there is a type mismatch. Something that does not happen if you use Any.

And as I mentioned, this code does not compile yet. The compiler is now complaining about some problems in our code.

Restricting the options of a generic and ensuring correctness using type constraints

Our code does not yet compile because our generic ModelType is, well, too generic.

At the moment, we can use the readFile(named:) method with any type whatsoever. While that gives us a great deal of flexibility, not all types in our app are decodable.

And that’s the problem here. The decode(_:from:) method of JSONDecoder wants only types that conform to Decodable. Otherwise, the decoder has no way of mapping the JSON data to the properties of a type.

To constrain the types we can use with a generic, we use type constraints. These restrict a generic only to types that descend from a specific class or conform to one particular protocol.

In our case, we want to our ModelType generic to conform to Decodable.

struct TestData {
	static let events: [Event] = readFile(named: "Events")
	static let participants: [Person] = readFile(named: "Participants")
	
	static func readFile<ModelType: Decodable>(named name: String) -> [ModelType]  {
		let url = Bundle.main.url(forResource: name, withExtension: "json")!
		let data = try! Data(contentsOf: url)
		let decoder = JSONDecoder()
		decoder.dateDecodingStrategy = .secondsSince1970
		return try! decoder.decode([ModelType].self, from: data)
	}
}

Now our code works. The compiler knows that any type we will use for our generic conforms to Decodable. And in case we try to use a type that doesn’t, the compiler will stop us from making mistakes.

That is also something that does not happen when you use Any.  In that case, the compiler has no information about the underlying type. 

Now we can see that the compiler stopped us because the decode(_:from:) method is also a generic function. That’s evident once you look at its declaration in the header file of the Foundation framework.

open class JSONDecoder {
	
	// ...
	
	open func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable
}

Here you can see another way of declaring type constraints on a generic. The Decodable constraint for T is in a where clause appended to the method declaration, instead of being in the declaration of the generic.

The generic types in the Swift standard library

In our TestData structure, only the readFile(named:) method is generic. But generics can also be used on entire types and not just functions.

The conventional Array and Dictionary data structures of the Swift standard library are good examples. This is another case of generics hiding in plain sight in your everyday code.

You can put values of any type inside arrays and dictionaries. And once a type for the content of a collection is decided, the compiler can check that all values are of the same type.

Now that we have seen how generics work, you can understand why collections work that way. Again, you can check their declaration and see that they use generics.

@frozen public struct Array<Element> {
	// ...
}

@frozen public struct Dictionary<Key, Value> where Key : Hashable {
	// ...
}

The Element and Value generics of Array and Dictionary don’t have any type constraint. So, you can put anything in a collection.

The Dictionary type also has a Key generic with a Hashable type constraint. That’s because a dictionary is a hash table that uses a hashing function to store its contents.

There is another generic type in Swift hiding in plain sight. The Optional type.

Swift makes a great job hiding optionals behind a ton of syntactic sugar. You declare optionals using the ? operator, use nil to represent absent values, and unwrap optionals using ?, ??, and !. You can also use conditional binding in conditional statements, i.e., if let or guard let.

But under the hood, optionals are nothing else than a generic enumeration containing two cases.

@frozen public enum Optional<Wrapped> : ExpressibleByNilLiteral {
	case none
	case some(Wrapped)
	// ...
}

The none case represents a nil value, while the some case contains a value, when present. Since optionals also need to work with any type, the Optional enumeration uses a Wrapped generic with no type constraints.

This means that with optionals, you can use any Swift construct that works with enumerations.

Write specific code first, and generalize only when needed

Arrays, dictionaries, and optionals are the typical generic type examples you find in many articles. Again, they show you how generics work, but they do not help you understand how to use them in your code.

Collections are necessary and understandable programming concepts. But generics are also useful in other, less evident, types.

The user interface for our app will provide a good example.

We already know from the design that the list of events and the list of participants have the same structure. Even though it is evident that they will need to share code, it’s always better to start writing specific code first.

Generalize code only later, when it becomes evident what needs to be generalized. Starting straight away with generic code is often a case of premature optimization.

While the concept is usually used in the context of code performance, it can be extended to the effectiveness of code writing in general.

You won’t know which parts of your code need to be generalized until you reach its limits.

Often, developers write needlessly general code. They think that, at some point, their code will need to be used with several types. But most of that code is, in reality, only used in one specific instance.

So, don’t make your code harder to read, only for the sake of some hypothetical future use cases that might never happen. Generalize code only when you need it to be generic.

We can create from the Join button for the table rows, which is a simple view that only requires basic parameters.

struct RowButton: View {
	let title: String
	let color: Color
	
	var body: some View {
		Text(title)
			.font(.subheadline)
			.bold()
			.foregroundColor(.white)
			.padding(EdgeInsets(top: 8.0, leading: 16.0, bottom: 8.0, trailing: 16.0))
			.background(color)
			.cornerRadius(20)
	}
}

struct EventsView_Previews: PreviewProvider {
	static var previews: some View {
		VStack(spacing: 8.0) {
			RowButton(title: "Join", color: .orange)
			RowButton(title: "Message", color: .blue)
		}
		.padding()
		.previewLayout(.sizeThatFits)
	}
}

 

the preview of the button for a row in the table

With that, we can create a view for the event rows in the table, and use that to create the full list of events.

struct EventsView: View {
	let events: [Event]
	
	var body: some View {
		NavigationView {
			List(events) { event in
				EventRow(event: event)
			}
			.navigationBarTitle("Events")
		}
	}
}

struct EventRow: View {
	let event: Event
	
	var body: some View {
		HStack(spacing: 16.0) {
			Image(event.title)
				.resizable()
				.frame(width: 70.0, height: 70.0)
				.cornerRadius(10.0)
			VStack(alignment: .leading, spacing: 4.0) {
				Text(event.title)
					.font(.headline)
				Group {
					Text(event.date.formatted(.full))
					Text("\(event.participants) people going")
				}
				.font(.subheadline)
				.foregroundColor(.secondary)
			}
			Spacer()
			RowButton(title: "Join", color: .orange)
		}
		.padding(.vertical, 16.0)
	}
}

struct EventsView_Previews: PreviewProvider {
	static var previews: some View {
		Group {
			EventsView(events: TestData.events)
			VStack(spacing: 8.0) {
				RowButton(title: "Join", color: .orange)
				RowButton(title: "Message", color: .blue)
			}
			.padding()
			.previewLayout(.sizeThatFits)
		}
	}
}

Creating custom protocols to constrain generic types

Now that we have some actual code we can analyze, we can generalize it to work with both the Event and Person types.

This is again a case where we need to parameterize a type. We know that we need to replace the Event type with a generic, but that piece of information alone does not bring us very far.

The problem is in the parts of our code that use specific properties of the Event type.

struct EventRow: View {
	let event: Event
	
	var body: some View {
		HStack(spacing: 16.0) {
			Image(event.title)
				.resizable()
				.frame(width: 70.0, height: 70.0)
				.cornerRadius(10.0)
			VStack(alignment: .leading, spacing: 4.0) {
				Text(event.title)
					.font(.headline)
				Group {
					Text(event.date.formatted(.full))
					Text("\(event.participants) people going")
				}
				.font(.subheadline)
				.foregroundColor(.secondary)
			}
			Spacer()
			RowButton(title: "Join", color: .orange)
		}
		.padding(.vertical, 16.0)
	}
}

The Person type does not have the title, date, and participants   properties.

In our specific example, we could rename the properties of Person to match those names, but that would not help anyway.

Any generic we add to our view would be independent of both the Event and Person types. It does not matter if they have common properties. Using a generic, we could not access any of them anyway.

Besides, that’s a bad practice anyway because a person does not have a title or participants. And in other cases, you might have types that are impossible to match anyway.

Every time we need to make assumptions on a generic, we need to use type constraints. In this case, though, we don’t have a protocol we can use.

The solution is to create a custom one.

Any type we display in a table row needs to have a headline,  two sub-headlines, an image, and so on.

protocol TableItem {
	static var navigationTitle: String { get }
	static var actionName: String { get }
	static var buttonColor: Color { get }

	var headline: String { get }
	var imageName: String { get }
	var subheadline1: String { get }
	var subheadline2: String { get }
}

Notice that I used both regular and static requirements in the TableItem protocol. That’s because headline, imageName, subheadline1, and subheadline2 are properties that change for each value, but navigationTitle, actionName, and buttonColor stay the same for all values of a specific type.

Using type constraints to make a Swift generic concrete

Now that we have a protocol defining the requirements of a row, we can use it as a type constrain for a generic.

And once a generic is constrained, you can treat it as an instance of that protocol because you know that the compiler will enforce its requirements.

struct Row<Item: TableItem>: View {
	let item: Item
	
	var body: some View {
		HStack(spacing: 16.0) {
			Image(item.imageName)
				.resizable()
				.frame(width: 70.0, height: 70.0)
				.cornerRadius(10.0)
			VStack(alignment: .leading, spacing: 4.0) {
				Text(item.headline)
					.font(.headline)
				Group {
					Text(item.subheadline1)
					Text(item.subheadline2)
				}
				.font(.subheadline)
				.foregroundColor(.secondary)
			}
			Spacer()
			RowButton(title: Item.actionName, color: Item.buttonColor)
		}
		.padding(.vertical, 16.0)
	}
}

The EventsView type contains the EventRow view, which we changed into the Row generic type. Any type that contains a generic one must either specify a type for the generic or expose that generic too. Here, we need the second option.

struct TableView<Item: TableItem & Identifiable>: View {
	let items: [Item]
	
	var body: some View {
		NavigationView {
			List(items) { item in
				Row(item: item)
			}
			.navigationBarTitle(Item.navigationTitle)
		}
	}
}

The Item generic of the TableView type must also conform to the Identifiable protocol since that’s required by the List view. In Swift, you compose protocols in type declarations using the & operator.

The final step is making our model types conform to both the TableItem and Identifiable protocols.

struct Event: Decodable, Identifiable {
	let title: String
	let date: Date
	let participants: Int
	
	var id: String { title }
}

extension Event: TableItem {
	static var navigationTitle: String { "Events" }
	static var actionName: String { "Join" }
	static var buttonColor: Color { .orange }
	
	var headline: String { title }
	var imageName: String { title }
	var subheadline1: String { date.formatted(.full) }
	var subheadline2: String { "\(participants) people going" }
}

struct Person: Decodable, Identifiable {
	let name: String
	let friends: Int
	let joined: Date
	
	var id: String { name }
}

extension Person: TableItem {
	static var navigationTitle: String { "Participants" }
	static var actionName: String { "Message" }
	static var buttonColor: Color { .blue }
	
	var headline: String { name }
	var imageName: String { name }
	var subheadline1: String { "\(friends) friends" }
	var subheadline2: String { "Joined \(joined.formatted(.long))" }
}

Using Swift extensions allows us to meet the requirements of TableItem without having to change the Event and Person types in any way.

Thanks to this addition, the compiler now allows us to use both types with our TableView generic structure.

struct TableView_Previews: PreviewProvider {
	static var previews: some View {
		Group {
			TableView(items: TestData.events)
			TableView(items: TestData.participants)
		}
	}
}

the previews of the generic SwiftUI view using two different types

Conclusions

Swift generics are a powerful language feature that allows us to abstract code in ways that would otherwise not be possible.

Thanks to generics, we can not only use values as parameters but also types. Moreover, the Swift compiler can match generics to types thanks to its automatic type inference, checking the correctness of our code.

Beware though that, like any other advanced feature, generics can make your code harder to understand.

Don’t reach for generics unless it’s necessary. Making code generic before it’s needed is a case of premature optimization, which makes your code unnecessarily complicated. Always start with specific code, and generalize it only when you get to a concrete situation that requires it.

Architecting SwiftUI apps with MVC and MVVM

It's easy to make an app by throwing some code together. But without best practices and robust architecture, you soon end up with unmanageable spaghetti code. In this guide I'll show you how to properly structure SwiftUI apps.

GET THE FREE BOOK NOW

2 thoughts on “Master Swift Generics: A practical guide to code reuse”

  1. Very interesting! I have seen before examples of using protocols to constraint generic types, but their usage was not very clear in my head (with gettable properties). Your explanation helped a lot. Thanks!

    Reply
  2. This was one of the best uses of my time in the 6months that I’ve been trying to learn SwiftUI! Certain things such as how and when to use completion closures, protocols, and generics have been mind-bending for me, and I’ve felt moments of hope that I understand mixed in with a lot of frustration. In particular, the swift.org examples always have left me baffled. This article is a phenomenally clear use case for generics and also, for me at least, perhaps the first time I have understood how to actually use protocols. Many thanks!

    Reply

Leave a Comment