Chapters

Hide chapters

Expert Swift

Second Edition · iOS 18.5 · Swift 6.1 · Xcode 16.3

4. Generics
Written by Marin Bencevic

Almost everyone using Swift — from complete beginners to seasoned veterans — has used generics, whether they know it or not. Generics power arrays and dictionaries, JSON decoding, optionals, Combine publishers and many other parts of Swift and iOS. Because you’ve already used many of these features, you know firsthand how powerful generics are. In this chapter, you’ll learn how to harness that power to build generics-powered features.

You’ll get intimately familiar with generics by rewriting the networking library you started in the previous chapter. This time, you’ll use generics to create a nicer API. You’ll learn how to write generic functions, classes and structs; how to use protocols with associated types; what type erasure is and how to put all that together to make a coherent API.

Starting a Generic Networking Library

Begin by opening the starter project. Just like in the previous chapter, you’ll make a networking library to download new articles from the Kodeco API to make your own Kodeco client. In the process, you’ll download different kinds of types, such as articles and images.

Open Networker.swift. This file will contain the main struct of your library used to initiate network requests. So far, though, it does nothing. You’ll begin by adding a function to fetch something from the network:

func fetch(url: URL) async throws -> Data {
  let (data, _) = try await URLSession.shared.data(from: url)
  return data
}

All this function does is route to the URLSession API. You’ll use it to fetch articles from the API and display them on ArticlesView. Head over to ArticlesViewModel.swift, and replace the contents of fetchArticles with the following:

let baseURL = "https://api.kodeco.com/api"
let path = "/contents?filter[content_types][]=article"
let url = URL(string: baseURL + path)!
do {
  // 1
  let articlesData = try await networker.fetch(url: url)
  // 2
  let decoder = JSONDecoder()
  let decodedArticles = try decoder
    .decode(Articles.self, from: articlesData).data
  articles = decodedArticles.map { $0.article }
} catch {
  // 3
  articles = []
}

Here’s what’s happening in the code above:

  1. Use your newly created function to fetch articles from the contents endpoint of the Kodeco API, fetching all content of type article.

  2. Next, use a JSON decoder to decode the response into an instance of Articles. Articles is a simple wrapper around a collection of ArticleData instances, each of which holds an article in its article property. This might sound a little confusing, so feel free to look at Article.swift to see how the structures map to the JSON response.

  3. If there’s an error in the fetching or decoding, don’t display any articles.

Build and run the project.

Your code works insofar as it fetches and displays all the articles. But you’d probably agree that the networking library you just made doesn’t have a huge number of features. For instance, it would be great if the library could also take care of the decoding. You’ll add that in the next section.

Generic Functions

To add decoding support to your library, you’ll add a new method that returns an already decoded type instead of Data. This presents a challenge: All kinds of types could be decoded, so what would the return type be for the method? One way to get around this would be to use Decodable as a return type since you know that any type returned from the function would have to implement that protocol.

func fetch(url: URL) async throws -> Decodable {
  let (data, _) = try await URLSession.shared.data(from: url)
  let decoder = JSONDecoder()
  let decoded = decoder.decode(???, from: data)
  return decoded
}

If you try to do this, however, you’ll soon run into an issue. When calling the decode(_:from:) method, you need to give it a concrete type to decode.

You can fix this by using a generic function. Open Networker.swift, and replace fetch(url:) with the following function:

func fetch<T>(url: URL) async throws -> T {
  let (data, _) = try await URLSession.shared.data(from: url)
  let decoder = JSONDecoder()
  let decoded = try decoder.decode(T.self, from: data)
  return decoded
}

The one special bit of syntax that makes this function generic is <T> in the function’s signature. While parentheses (()) surround the function’s parameters, angle brackets (<>) surround the function’s type parameters. A generic function receives type parameters as part of a function call, just like it receives regular function parameters.

Of course, like regular parameters, you can have multiple comma-separated type parameters:

func replaceNils<K, V>(
  from dictionary: [K: V?],
  with element: V
) -> [K: V] {
  dictionary.compactMapValues {
    $0 == nil ? element : $0
  }
}

In your case, there’s only one type parameter: T. The name of this parameter isn’t some magic, special constant — you can call it whatever you want. In the example above, you could’ve used ValueToDecode or anything else.

When your function is very generic, and the type can be almost any type, it’s fine to use single-letter type parameter names like T and U. But more often than not, your type parameter will have some sort of semantic meaning. In those cases, it’s best to use a more descriptive type name that hints at its meaning to the reader. For example, instead of using single letters, you might use Element in a collection, Output in some sort of long operation, etc.

Once you define the type parameter inside the angle brackets, you can use it in the rest of the function’s declaration and inside the function’s body. In this example, you used it both as the return type of the function as well as in the body as a parameter given to decode(_:from).

You might’ve noticed that the code still doesn’t work. Xcode gives you the following error: “Instance method decode(_:from:) requires that T conform to Decodable”. As the error says, you’re passing T.self to decode(_:from:), but there’s no way for Swift to guarantee that T conforms to Decodable.

Thankfully, you can fix that by replacing the function’s signature with the following:

func fetch<T: Decodable>(url: URL) async throws -> T {

Here, you’re telling Swift that T must be some type conforming to Decodable. You’re using Decodable as a generic constraint: a way to tell Swift which types are accepted for the generic type parameter. Now, the compiler is happy because it can guarantee that T will always be Decodable, and the error disappears.

Head back to ArticlesViewModel.swift, and replace the do block of fetchArticles with the following:

let articlesData: Articles =
  try await networker.fetch(url: url)
articles = articlesData.data.map { $0.article }

Here, you call the generic function you wrote earlier. You specify that the type of articlesData is Articles. In fetch(url:)’s signature, you defined that the function returns a T value. When you assign the return type of the function to articlesData, Swift knows that articlesData has a type of Articles and can figure out that it needs to replace T with Articles. Swift is smart like that.

Whenever you call a generic function, you can think of the process as copying the generic function and then substituting the type parameter with the actual concrete type you’re working with. This allows you to create a single function that works across all possible types, saving you from having to copy and paste functions.

In a sense, generics are the opposite of protocols. Protocols let you define the same function signature for multiple types, each with its own implementation. Generics let you define a single implementation of a function and use that implementation on multiple types.

Build and run the project.

The app has no visible changes, but you’ve saved yourself a couple of lines of code in the view model. More importantly, think of all the future lines of code you’ve saved! For each network request in your app, you now have a function that can download and decode anything with just one line.

Simplifying Generic Functions With some

You’ll often write simple generic functions that involve only one type parameter. For instance, say you want to add HTTP POST capability to your library by adding the following function to Networker:

func upload<T: Encodable>(
  _ value: T, to url: URL
) async throws -> URLResponse {
  let encoder = JSONEncoder()
  let json = try encoder.encode(value)
  var request = URLRequest(url: url)
  request.httpMethod = "POST"
  request.httpBody = json
  let (_, response) = try await URLSession.shared.data(for: request)
  return response
}

The function above is generic over any Encodable type, as can be seen from its type parameters. The function first encodes the value into JSON data and then makes an HTTP POST request with that data.

The function’s signature is pretty complex. In cases like this, where you have a simple generic function, Swift lets you use the some keyword to simplify it:

func upload(
  _ value: some Encodable, to url: URL
) async throws -> URLResponse {

Now, instead of using a named type parameter, the type of value becomes some Encodable. This is just syntax sugar, and the function is completely identical to the one above, but it does make it easier to read and understand.

You can use this syntax when you mention the parameter’s type only once, i.e., neither the rest of the function’s signature nor the body requires the name of the parameter’s type. If you’re in doubt, you can always start out using some and then give the type an explicit name when the need arises while you’re writing the function.

Don’t forget that you can also use some as a return type from a function, just like in a SwiftUI view’s body, let’s dive into more ways it can be utilized in Swift..

Using Opaque Types

Types like some Decodable as well as T: Decodable aren’t like other types. Instead of being some concrete implementation with defined methods and variables, these types are more like placeholders that can hold all kinds of types, so long as the condition of the type implementing Decodable is satisfied. These kinds of types are called opaque types since they obscure their concrete implementation.

An opaquely typed value will always have a fixed concrete type throughout the scope of the value. For instance, if you use an opaque type as a function parameter, whenever you access that parameter inside the function, it’ll always have the same concrete underlying type. This becomes especially important for opaque return types:

func getInitialRequest(for view: IntialView) -> some Request {
  if view == .articles {
    return ArticleRequest()
  } else {
    return ImageRequest(url: welcomeImageURL)
  }
}

Remember, some types need to be fixed throughout their scope. In the function above, the return type needs to be fixed throughout the function. Yet, the function tried to return an ArticleRequest in the if statement and an ImageRequest in the else block.

Even though both types implement Request, Swift still gives you a compiler error, saying the return statements don’t have the same underlying concrete type. You can get around this issue by using type erasure, which you’ll learn about in the next section of this chapter.

Generic Types

So far, the app only displays articles, but some users would surely like to mark articles to read at a later time. In this section, you’ll add a way to do this. The list of articles needs to persist between app launches, so you’ll need a way to store a list of the user’s favorite articles. For this app, you’ll store the IDs of the articles marked to be read later in User Defaults.

Create a new Swift file called UserDefaults.swift, and add the following struct to the file:

import SwiftUI

struct UserDefaultsValue<Stored> {
  let key: String

  var value: Stored? {
    get {
      UserDefaults.standard.value(forKey: key) as? Stored
    } set {
      UserDefaults.standard.set(newValue, forKey: key)
    }
  }
}

Like generic functions, generic types also have type parameters, declared right next to their type name. Once you declare a struct in this way, you can use the type parameter throughout in the struct. This isn’t limited to structs, however. You can use the same syntax to define generic classes and even enums.

Note: Optional is an example of a generic enum! Look at its implementation — it’s simpler than it seems.

Here, you wrote a generic struct that can store and retrieve any type from UserDefaults. There’s a clear advantage to this struct over using UserDefaults directly: type safety. By only using the value property, Swift guarantees that the value stored will always be of type T, removing casting and checking from your code.

Next, you’ll use your newly learned generics chops to write a generic function that will append an element to stored arrays:

mutating func append<Element>(
  _ element: Element
) where Stored == Array<Element> {
  if let value {
    self.value = value + [element]
  } else {
    self.value = [element]
  }
}

As the where clause suggests, this function will only be available when Stored is an array. In those cases, this function will append a value whose type matches Stored’s Element type to user defaults. The function adds the element to the array if it exists. Otherwise, it saves an array of just that element.

Next, head back to ArticlesViewModel.swift. Add the following code to readLater, which gets called from a swipe action in the articles list when the user taps Read Later:

var savedArticles = UserDefaultsValue<[String]>(
  key: "savedArticles")
savedArticles.append(article.id)

To use a generic type, you provide a concrete type inside angle brackets next to the type’s name. Here, Swift replaces T with [String]. The process of replacing a type parameter with a concrete type value is called specialization. In this case, typing <[String]> is necessary because Swift doesn’t have a way to infer it. In other cases, when you use the type parameter in the initializer, Swift can figure out what your concrete type is without you having to use angle brackets.

Next, you’ll use this array of saved articles as a filter for the Read Later tab in the app. Add the following function to the class:

func reloadSavedArticles() {
  let storedSavedArticles = UserDefaultsValue<[String]>(
    key: "savedArticles")
  if let savedArticleIDs = storedSavedArticles.value {
    savedArticles = articles.filter {
      savedArticleIDs.contains($0.id)
    }
  }
}

You’ll use this function to update savedArticles whenever articles gets updated. You go through all the articles and only keep the ones stored in User Defaults.

Next, add a call of this function at the bottom of fetchArticles:

reloadSavedArticles()

Then, add the same function call to the end of readLater(_:):

reloadSavedArticles()

Now savedArticles will get updated whenever the user saves or fetches the articles.

Build and run your app.

Use a swipe action to save a couple of articles, then head to the Read Later tab. You’ll see the tab get filled with the articles you just saved, making your app much more useful for your users.

Understanding Type Invariance

UserDefaultsValue itself is not a type. If you try to use UserDefaultsValue without type parameters as the type of variable, you get a compiler error. Swift recognizes only specialized variants of the type, such as UserDefaultsValue<String>, as real types. Generic types by themselves are more like a blueprint: a type of scaffolding for you but not of much use to the compiler.

When you specialize a generic type, it’s as if Swift copies your struct and replaces all mentions of the generic type parameters with the type you specified. This sometimes leads to confusing type relationships:

let value = UserDefaultsValue<Int>(key: "index")
let comparable: UserDefaultsValue<Comparable> = value

value is a UserDefaultsValue<Int>, and Int definitely conforms to Comparable. You might think, then, that you can store an instance of UserDefaultsValue<Int> as an instance of UserDefaultsValue<Comparable>. Nope! This results in a compiler error.

While Comparable is a supertype of Int, as far as Swift is concerned, there’s no such relationship between the two UserDefaultsValue variants. They’re completely unrelated types. These kinds of generic types are called invariant types.

This behavior doesn’t apply to built-in generic types, though. By way of Swift compiler magic, types like Array, Collection and Optional are not invariant:

class Animal { }
class Cow: Animal { }

let cows: Array<Cow> = [Cow()]
let animals: Array<Animal> = cows

The code above does a similar thing as the earlier example. Array is a generic type, cows is an array of subtypes (Cow), and it’s being assigned to an array of its supertype (Animal). This time, there’s no compiler error because Array is covariant and not invariant. Unfortunately, so far, there’s no way for you to write generic types like this.

This is just one of the many quirks of generic types. The next section explains another one.

Creating Generic Collections With Type Erasure

A similar issue crops up when you want to declare a heterogenous array of your generic type with different type parameters:

func clearUserDefaults(_ storedValues: [UserDefaultsValue]) {
  for storedValue in storedValues {
    storedValue.value = nil
  }
}

The code above doesn’t compile because there’s no such thing as a UserDefaultsValue type — it always needs to be specialized.

One way to get around this issue is to create a non-generic wrapper around the type:

protocol AnyUserDefaultsValue {
  var key: String { get }
  var anyValue: Any? { get set }
}

extension UserDefaultsValue: AnyUserDefaultsValue {
  var anyValue: Any? {
    get { value }
    set { value = newValue as? Stored }
  }
}

AnyUserDefaultsValue will be a non-generic protocol. Since it no longer has access to Stored, the only type-safe way to access value is to cast it to Any?, which is done by the anyValue property.

Now, the clearUserDefaults function can use an array of AnyUserDefaultsValue since it’s not a generic type:

func clearUserDefaults(_ storedValues: [AnyUserDefaultsValue]) {
  for var storedValue in storedValues {
    storedValue.anyValue = nil
  }
}

And, since UserDefaultsValue conforms to AnyUserDefaultsValue, you can call the function with an array of different kinds of values:

clearUserDefaults([
  UserDefaultsValue<Int>(key: "index"),
  UserDefaultsValue<String>(key: "userName")])

This technique of hiding a generic type inside a non-generic wrapper is called type erasure. By creating AnyUserDefaultsValue, you’ve erased information about the concrete type T and instead used Any. By removing type information, you allow the type to be more abstract and thus be used as an element in a collection.

For protocols, there’s a built-in way to achieve type erasure, which you’ll learn more about in the following section.

Protocols With Associated Types

So far, you’ve learned how to define generic functions, structs, classes and enums. You still need to conquer one more kind of generic types: generic protocols. In Swift, they’re called protocols with associated types, or PATs for short.

In your view model, you’re currently hard-coding a URL used to fetch the articles. This means you’d have to copy and paste this URL throughout your codebase whenever you want to fetch articles. Instead of doing that, you’ll add a protocol to represent an HTTP request in your library. Then, you’ll create an implementation of that protocol to represent an article request.

Open Request.swift. This file contains a simple enum that represents the different types of HTTP methods. Add the following protocol to the file:

protocol Request {
  associatedtype Output

  var url: URL { get }
  var method: HTTPMethod { get }
  func decode(_ data: Data) throws -> Output
}

PATs are structured a little differently than other generic types. Instead of the generic type being a parameter of the protocol, it’s one of the protocol’s requirements, like protocol methods and properties. You declare associated types using the aptly named associatedtype keyword.

The Output type tells the user what this request is supposed to fetch. Its name is a placeholder, just like the names of type parameters in generic types. It can be an Article, [Article], User, etc. The decode(_:) function is responsible for converting the data received from URLSession into the output type.

To conform to a PAT, you need to declare the associated type just like you would with property and method requirements. You’ll add a new struct to represent the request to fetch new articles. Start by adding the following to the end of the file:

struct ArticleRequest: Request {
  typealias Output = [Article]
}

The first protocol requirement you’ll satisfy is the associated type. The way you do this is by adding a typealias with the same name as the name of the associated type declared in the protocol. In this case, Output needs to become [Article] since that’s what you’ll return from Decode.

Next, continue writing the struct by implementing the rest of the protocol’s requirements:

var url: URL {
  let baseURL = "https://api.kodeco.com/api"
  let path = "/contents?filter[content_types][]=article"
  return URL(string: baseURL + path)!
}

var method: HTTPMethod { .get }

func decode(_ data: Data) throws -> [Article] {
  let decoder = JSONDecoder()
  let articlesCollection = try decoder
    .decode(Articles.self, from: data)
  return articlesCollection.data.map { $0.article }
}

You provide access to a URL to fetch the articles, an HTTP method and, finally, give the struct a way to decode the fetched data.

Next, try to delete the typealias you just wrote and recompile the project. You might be surprised that there aren’t any errors. Well, Swift is smart enough to infer the associated type from the struct’s implementation. Because the protocol declares that the decode(_:) method returns Output, and ArticleRequest‘s decode(_:) returns [Article], Swift understands that [Article] is the Output. In a lot of cases, you don’t need to declare an explicit type alias for PATs, Swift will infer it.

Now that you have a generic way to represent a request, you can update Networker to use this new protocol. Open Networker.swift and add a new method to the class:

func fetch<R: Request>(_ request: R) async throws -> R.Output {
}

This method will be generic over any type that implements a request. Its return type will also be generic, determined by the concrete implementation of Request when the method is called. If you call the method with ArticleRequest, its return type will be [Article] since ArticleRequest declared Output to be [Article]. Pretty nifty, right?

Next, write the body of the method:

var urlRequest = URLRequest(url: request.url)
urlRequest.httpMethod = request.method.rawValue
let (data, _) = try await URLSession.shared
  .data(for: urlRequest)
let decoded = try request.decode(data)
return decoded

Here, you create a URLRequest with the provided information and send it over. Then, you decode the result using the decode(_:) method of whichever concrete implementation of Request the method was called with.

Finally, you can use your new method in the view model. Open ArticlesViewModel.swift, and replace the contents of fetchArticles with the following:

do {
  articles = try await networker.fetch(ArticleRequest())
} catch {
  articles = []
}

reloadSavedArticles()

By using PATs and generic functions, you made your network calls incredibly simple to read and write. Build and run the project to make sure everything still works.

Once again, the app has no visible changes, but the code has become a lot cleaner than it was when you started! But there’s no reason to stop there. You can use PATs to turbo-charge your networking library by adding automatic decoding. Keep reading to find out how.

Extending PATs

Currently, each implementation of Request has to provide its implementation of decode(_:). In most cases, Output will be some Decodable value that already has its own method to be decoded. To save the users of your API from having to write out a boilerplate decode(_:), you’ll provide a default implementation when Output is already Decodable. The way you’ll do this is with an extension.

Head over to Request.swift, and add the following extension to the file:

extension Request where Output: Decodable {
  func decode(_ data: Data) throws -> Output {
    let decoder = JSONDecoder()
    return try decoder.decode(Output.self, from: data)
  }
}

When you create a Request implementation whose type conforms to Decodable, you’ll get this implementation for free. It’ll try to use a JSON decoder to return the Output type.

You built your networking library with the assumption that every request returns some sort of response in the form of the Output type. However, sometimes you’re downloading a large piece of data from the server and don’t expect a direct response, but a URL of the downloaded file.

Your next step will be to add support for Data requests throughout your networking library so that making these types of requests is easy for the end user. First, you’ll make sure to provide a default decode(_:) implementation when the Output is Data. The way to do this is by using another extension, so add the following to the file:

extension Request where Output == Data {
  func decode(_ data: Data) throws -> Output {
    return data
  }
}

Aside from checking for protocol conformance or class inheritance, you can constrain an extension to a specific type using ==. Here, you add decode(_:) to all Request types whose Output is Data, which will just pass the received data along.

The Swift standard library uses a similar trick with extensions to conform a generic type to a protocol when its type parameter conforms to a certain protocol. More concretely, an array of equatable values will itself become equatable, even though Array itself isn’t equatable in all cases. You can see this as an extension in Swift’s source code:

extension Array: Equatable where Element: Equatable {

Keep this little trick in mind when designing your own APIs in the future! For now, follow along with the next section to keep adding support for Data requests.

Adding Primary Associated Types

Your next step to adding support for Data requests is writing a method that returns a downloaded file URL instead of returning the output directly. URLSession defines a download(for:delegate:) method to do this exact thing, but there’s currently no way to access that using Networker. You’ll fix that next.

Head over to Networker.swift, and add the following method to the class:

func download<R: Request>(
  _ request: R
) async throws -> URL where R.Output == Data {
  var urlRequest = URLRequest(url: request.url)
  urlRequest.httpMethod = request.method.rawValue
  let (url, _) = try await URLSession.shared
    .download(for: urlRequest)
  return url
}

Similarly to the extension you wrote earlier, you constrain this method to be generic over all Requests, but only when the Output is Data. In the function, you use the built-in download(for:delegate:) to download something to a file and return the file’s URL.

There’s a slightly simpler way to write this using the some keyword you learned about earlier. However, it requires a primary associated type. Head back to Request.swift, and change the declaration of Request to the following:

protocol Request<Output> {

You added a type parameter to the protocol, just like type parameters in structs and classes. This type parameter tells Swift that Output is the primary associated type of this protocol. You still need the associatedtype inside the protocol, though, and the name of the associated type needs to match the type parameter. Primary associated types can be used to constrain the protocol using some and any, which you’ll learn about in the next section.

Replace the signature of the download(_:) you just wrote in Networker.swift with the following:

func download(
  _ request: some Request<Data>
) async throws -> URL {

A function parameter of type some Request<Data> could hold any concrete Request implementation whose Output is Data. In other words, this declaration does the same thing as the one from earlier, but it’s much easier to read. You can also use some to constrain Output to a protocol, for instance, some Request<Decodable>. Note that you can only add constraints with the some to primary associated types. However, you can always fall back on using where for other associated types.

Now that you have this function, you can use it to download a video course. The sample project won’t use this, but here’s how you’d do it. First, you’d define a new Request pointing to a video URL:

struct VideoRequest: Request {
  typealias Output = Data
  var url: URL {
    URL(string:
      "https://player.vimeo.com/external/332761683.sd.mp4")!
  }
  var method: HTTPMethod { .get }
}

Because you defined an extension of Request types whose Output is Data, there’s no need to implement decode(_:) here.

Next, you’d call your newly created Networker method using the request:

let fileURL = try await networker.download(VideoRequest())

Remember that download(_:) is a generic method that can use any kind of request type to download files, so long as their Output is Data.

With just a few extensions and a new generic method, you enabled a whole new class of requests to be used without any boilerplate code. As long as you’re clever with how you use them, PATs can enable incredibly powerful features.

Make sure to use primary associated types whenever the associated type is crucial to the functioning of the protocol. For instance, the Element type of a collection is a primary associated type. This will allow you to constrain the type in generic functions and extensions in the future, as well as let you create heterogenous collections of PATs. More on that in the next section!

Using Type Erasure for PATs With any

Next, you’ll upgrade your networking library to add support for initiating multiple requests at once. You’ll do this by creating a method that receives an array of different kinds of requests. Open Networker.swift and add the following function to the file:

func fetchAll(_ requests: [Request]) async throws {
}

When you do this, you’ll get an error because, just like other generic types, PATs are not types. They’re just placeholders for a concrete type. Swift tells you that you can’t use Request as a type unless you add any in front of it. But what does any even do?

Click the Fix button to let Swift fix the error above for you, and you’ll end up with the following:

func fetchAll(_ requests: [any Request]) async throws {

Swift has added the keyword any in front of Request. To you, this might not look like a significant change, but to Swift, now the function is completely different. You already know that in Swift, you can’t have an array of different types, like an array of String and Int. But you can have an array of Any since every type conforms to Any. The lowercase any keyword behaves similarly but lets you constrain the types inside the array a little bit more, for instance, to any type that conforms to a protocol.

If this sounds familiar, it’s because you’ve already learned about this technique earlier when you learned about type erasure. This is type erasure for PATs, but instead of making your own non-generic types, the any keyword does that for you.

You can think of any as a box — the box has Request written on it and, thus, can hold a concrete implementation of Request, but you don’t know exactly which implementation at compile time. At run time, Swift opens the box and uses the concrete type inside. any types are kind of like pointers to a concrete type.

Another name for any types is existential types. The key feature of any types is that they can hold any subtype, just like an instance of a superclass can hold any of its subclasses. Interestingly, this also includes some types. some Request is always some concrete implementation of request, and thus is a subtype of any Request. The compiler is perfectly happy with you passing some Request to a function accepting any Request.

Continue writing your function to fetch the requests:

for request in requests {
  let output = try await fetch(request)
}

Option-Click the output variable, and you’ll notice its type is Any. You know that fetch returns Request.Output, the generic associated type. However, in this function, you used the boxed any Request. Since this box can contain any potential implementation of Request, Swift can’t know what the concrete type of Resource will be at compile time. So, Swift is forced to use the most general type it can — Any. In this case, Any is the upper bound of Request.Output.

Instead of returning Any, you can constrain the function to work only on requests where the primary associated type implements Decodable and then returns the responses. Change the function declaration to the following:

func fetchAll(
  _ requests: [any Request<Decodable>]
) async throws -> [Decodable] {

If you look at output‘s type now, you’ll see it’s any Decodable since that’s the new upper bound. Remember that you can only do this with primary associated types.

Note: While powerful, heterogenous collections of PATs with the any keyword are computationally expensive. Swift needs to treat each value as a generic box, which prevents it from using clever optimizations. Whenever possible, use regular generic types or the some keyword over any.

In fact, you can even go a step further and make the function generic. Replace the declaration with the following:

func fetchAll<Output>(
  _ requests: [any Request<Output>]
) async throws -> [Output] {

Now, you don’t need to know the type of the Output ahead of time — it can be determined by the caller of the function. Next, replace the contents of the function with code to execute the requests and return the responses:

var outputs: [Output] = []
for request in requests {
  outputs.append(try await fetch(request))
}
return outputs

Now that you have this function, you’ll use it to initiate a request to download images for the articles. You’ll begin by creating a new Request, which will be able to download images. Open Request.swift and add a new struct to the file:

struct ImageRequest: Request {
  let url: URL
  var method: HTTPMethod { .get }
}

You’ll provide the URL of the image in the struct’s initializer. Next, implement decode(_:) in the struct:

func decode(_ data: Data) throws -> UIImage {
  if let image = UIImage(data: data) {
    return image
  } else {
    throw DecodingError.typeMismatch(
      UIImage.self,
      DecodingError.Context(
        codingPath: [],
        debugDescription: "No image in data."))
  }
}

You try to convert the data to an image. If the operation fails, you’ll throw a DecodingError with some more information.

Next, head over to ArticlesViewModel.swift to start using your new method and struct. Add the following code to the end of the do block inside fetchArticles, right after you get the articles:

let imageRequests = articles
  .compactMap(\.image)
  .map(ImageRequest.init)
let images = try await networker.fetchAll(imageRequests)
for i in 0..<images.count {
  articles[i].downloadedImage = images[i]
}

You create an ImageRequest for each of the articles you fetched, and then you call the new fetchAll(_:) with those requests. Once you get the images, you go through the articles’ array and add each image to its corresponding article. SwiftUI will detect this change and reload the view to show the image.

Build and run the project.

When the app loads, you’ll see all the articles. Then, after a moment, the images will also pop up once the app finishes downloading them — all in just a few lines of code of your view model!

You now know how to write generic functions, types and protocols. You know how to extend and combine them. You also know how to use the some and any keywords effectively. On top of all of that, you’re armed with a bunch of theory that will guide your API design in the future. But there’s one small thing left to cover, the issue of self.

Self and meta-types

“What is the self?” is a philosophical question. More important for this chapter is explaining what self is as well as what Self and T.self are. These are much easier to answer than the philosophical question. Although this section might not relate directly to generics, it has a lot to do with the type system itself. And understanding the different selves and ways to use types in Swift will help you better understand generics.

As you already know, self is usually a reference to the object whose scope you’re currently in. If you use self inside an instance method of a User struct, self will be that instance of that struct. So far, that’s pretty straightforward. However, when you’re in a static method of a class, self can’t be a reference to an instance because there is no instance: You’re in the class itself.

class Networker {
  class func whoAmI() {
    print(self)
  }
}

Networker.whoAmI() // "Networker"

In class and static methods, self has the value of the current type, not an instance. It makes sense when you think about it: Static and class methods exist on the type, not an instance.

However, all values in Swift need to have a type, including the self above. After all, you need to be able to store it in variables and return it from functions. What would be the type that holds self in class and static methods that you now know holds a type? The answer is Networker.Type: a type encompassing all Networker subtypes! Just like Int holds all integer values, Int.Type holds all Int type values.

These types that hold other types are called meta-types.

It kind of makes your head spin, right?

class WebsocketNetworker: Networker {
  class func whoAmI() -> Networker.Type {
    return self
  }
}

let type: Networker.Type = WebsocketNetworker.whoAmI()
print(type)

In the example above, you declare a meta-type variable called type. The meta-type can hold not only the Networker type itself but also all of its subclasses, such as WebsocketNetworker. In the case of protocols, a meta-type of a protocol (YourProtocol.Type) can hold the protocol type as well as all concrete types conforming to that protocol.

To use a type itself as a value, such as to pass it to a function or store it in a variable, you need to use Type.self:

let networkerType: Networker.Type = Networker.self

You have to do this for practical reasons. Usually, type names are used to declare the type of a variable or function parameter. When they’re not used for declaring types, they’re used implicitly as initializers. Using .self makes it clearer that you need the type as a value rather than as the type of something else and that you’re not calling an initializer.

Finally, there’s Self with a capital “S”. Thankfully, this one is less convoluted than all this meta-talk. Self is always an alias to the concrete type of scope in which it appears. Concrete is emphasized because Self will always be a concrete type, even if it’s used inside a protocol method.

extension Request {
  func whoAmI() {
    print(Self.self)
  }
}

ImageRequest().whoAmI() // "ImageRequest"

Self is useful when you want to return the current concrete type from a protocol method or use it as an initializer inside a static method when creating factory methods.

In generic contexts or when working with Any, you can’t know the concrete type of a value until the function runs. Sometimes, however, you need a reference to the concrete type, for instance, to cast a value. In those cases, you can use type(of:), a built-in method that will return the underlying true type of some value:

func getUserDefaultsInfo(forKey key: String) {
  let value = UserDefaults.standard.value(forKey: key)
  print(type(of: value))
}

Even though Value is of type Any, this function will print Int if the stored value is an integer.

Note that the use of type(of:), especially in generic contexts, is often a code smell, i.e., a sign that you’re probably doing something wrong. The same goes for checking types in an if block or casting to concrete types in generic functions. Generics should be, by definition, general across all types. If you’re making parts of your function depend on the concrete type, it’s a sign that you need to rethink your function. Keep that in mind when writing your APIs!

Whew! That sure was a whirlwind of some pretty advanced concepts. Hopefully, this chapter has given you a good overview of generics in Swift and will serve as a useful reference when you need to refresh your memory on these concepts.

Key Points

  • Methods, structs, enums and classes all can become generic by adding type parameters inside angle brackets.
  • Protocols can be generic as well through the use of protocols with associated types.
  • You can use extensions with generic constraints, using the where keyword to extend generic types when their type parameters satisfy specific requirements.
  • You can also specialize methods themselves by using the where keyword.
  • Use type erasure to use generics and PATs as regular types.
  • some types are opaque and are a placeholder for a concrete type, but the type is fixed for the scope of the value.
  • any types are existential and act as a box around a concrete type, holding any subtype of the type.
  • self has the value of the current type in static methods and computed properties, and the type of self in those cases is a meta-type.
  • Self always has the value of the current concrete type.

Where to Go From Here?

Now that you’re more familiar with generics, you can explore the many generic types inside Swift itself. For instance:

If you want even more introduction to generics, check out the Embrace Swift Generics WWDC22 session. For more information on PATs and primary associated types, watch the excellent Design protocol interfaces in Swift.

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.