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.
- This article is part of the Learn Swift series.
FREE GUIDE - SwiftUI App Architecture: Design Patterns and Best Practices
MVC, MVVM, and MV in SwiftUI, plus practical lessons on code encapsulation and data flow.
DOWNLOAD THE FREE GUIDEContents
- You can’t understand generics until you find concrete examples that require them
- Most of the code you write uses generics invisibly to make your life simpler
- Generalizing the return type of methods
- Using Swift generics to parametrize types in functions
- Restricting the options of a generic and ensuring correctness using type constraints
- The generic types in the Swift standard library
- Write specific code first, and generalize only when needed
- Creating custom protocols to constrain generic types
- Using type constraints to make a Swift generic concrete
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.
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 thereadFile(named:)
, we have to cast its result to our desired type using eitheras?
oras!
. - 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)
}
}
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)
}
}
}
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.
SwiftUI App Architecture: Design Patterns and Best Practices
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.
Matteo has been developing apps for iOS since 2008. He has been teaching iOS development best practices to hundreds of students since 2015 and he is the developer of Vulcan, a macOS app to generate SwiftUI code. Before that he was a freelance iOS developer for small and big clients, including TomTom, Squla, Siilo, and Layar. Matteo got a master’s degree in computer science and computational logic at the University of Turin. In his spare time he dances and teaches tango.
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!
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!