Swift — Generics

Generics are a key feature in the Swift language.

« 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. »

Although the part « types that can work with any type » might raise some concerns regarding the way our objects are supposed to work with undefined types, the answer is pretty straightforward.

Because Swift is a type-safe language, a concrete type will always be tied to a specific generic at compile time.

The standard library uses plenty of generic code.

One of the most commonly used data types in an app actually use generics.

There are two different ways to declare an array.

let numbers: [Int] = [1, 2, 3, 4]
let numbers: Array<Int> = [1, 2, 3, 4]

Under the hood, Array<Element> takes a generic type which is yet to be defined.

A quick look at the standard library.

public enum Optional<Wrapped> : ExpressibleByNilLiteral {// The compiler has special knowledge of Optional<Wrapped>, including the fact// that it is an `enum` with cases named `none` and `some`./// The absence of a value./// In code, the absence of a value is typically written using the `nil`literal rather than the explicit `.none` enumeration case.case none/// The presence of a value, stored as `Wrapped`.case some(Wrapped)}

The Optional type — which is an Enum — takes a Wrapped generic type.

An optional could be declared in two different ways.

var number: Int? = 5
var number: Optional<Int> = 5

While the former is commonly used when it comes to declaring an optional value, the latter gives us a clear vision on how the generics work in the Swift language.

When declaring your variable, the Optional generic type is being specialized that is being given a concrete type.

Generics are declared using <> with a nested keyword such as <Element> or <Wrapped>.

Keywords could really be any words but it rather be explicit about its role.

public struct Dictionary<Key: Hashable, Value>

Using ‘Key‘ and ‘Value‘ as keywords give us quite a hint about what the concrete types will actually represent in the Dictionnary struct.

Generics are supported by classes, structs, functions.

class TableViewDataSource<Model>: UITableViewDataSource {
let model: [Model]
init(_ model: [Model]) {
self.model = model
}
}
struct Resource<DataModel> {
let url: URL
let parse: (AnyObject) -> DataModel
}
func fetch<DataModel>(
resource: Resource<DataModel>,
completionHandler: (DataModel) -> ()
) { }

Protocols support generics although the annotation is a bit different.

protocol SomeProtocol {
associated type SomeGeneric
var someProperty: SomeGeneric
}

The associated type keyword is the protocols’ equivalent generic annotation of <Element> or <Wrapped>.

Generic enums are very convenient when it comes to representing commons scenarios with different associated values.

enum State<Content> {
case loading
case loaded([Content])
case error(Error)
}
let evens: State<Int> = .loaded([2, 4, 6, 8])
let names: State<String> = .loaded(["John", "Joe", "Jack"])

Declaring such a generic enum gives us more flexibility while avoiding code duplication as we could decide to deal with a unique enum — with some specific content — throughout the app.

Specific constraints can be set on generics in order to narrow down the scope of specialization.

In fact, to fully embrace the power of generics, it might often be necessary to define specific type constraints in order to make our implementation actually work.

Let’s try to build a generic fetch() function to retrieve some data, decode it properly and return a concrete object.

enum Error: Swift.Error {
case url
case decode(Swift.Error)
case network(Swift.Error)
}
func fetch<Model: Decodable>(
withUrl url: URL?,
decodeType: Model.Type,
completionHandler: @escaping (Result<Model, Error>) -> ()
) {
guard let url = url else {
completionHandler(.failure(Error.url))
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
// Your session’s datatask here.
let data = Data() do { let models = try JSONDecoder().decode(
decodeType,
from:data
)
completionHandler(.success(models)) } catch let error {
completionHandler(.failure(Error.decode(error)))
}
}
}

The compiler would raise an error if we didn’t specify a ‘Decodable‘ type constraint.

Specifying a generic constraint is a way of narrowing down the options regarding what the concrete type could be in order to provide the compiler with enough informations.

Now that the compiler knows that the generic ‘Model’ type will be a decodable object, it is able to properly use the decode<T>(_ type: T.Type, from data: Data) function.

Defining type constraints is quite simple.

func fetch<Model: Decodable>
func fetch<Model> where Model: Decodable

Both are equivalent.

Although the comparison is legitimate, Any and Generics behave differently.

‘Any‘ underlying type will be casted at runtime — which is error prone — whereas Generics will be statically checked at compile time.

Thanks to the beauty of Swift language’ type safety, combining generic types with protocol type constraints — such as in previous example — is very powerful when it comes to local specialization.

Phantom types are used to provides additional safety in your code.

Their types is often more important than their actual implementation. They may even not have one.

Going back to our previous example using the fetch() method, let’s assume we use several fetch() methods gathered in a network manager with different implementation according to some authorization level.

Calling then independently from within the app might end up, at some point, in calling the wrong one which will definitely lead to some undesired side effects.

Let’s create our manager and implement our fetch() methods.

class NetworkManager {    func fetch<Model>(
withUrl url: URL?,
decodeType: Model.Type,
completionHandler: @escaping (Result<Model, Error>) -> ()
) where Model: Decodable {
guard let url = url else {
completionHandler(.failure(Error.url))
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
// session’s datatask here.
let data = Data() do {
let model = try JSONDecoder().decode(
decodeType,
from: data
)
completionHandler(.success(model)) } catch let error {
completionHandler(.failure(Error.decode(error)))
}
}
}
func fetch<Model>(
withUrl url: URL?,
decodeType: Model.Type,
completionHandler: @escaping (Result<[Model], Error>) -> ()
) where Model: Decodable {
guard let url = url else {
completionHandler(.failure(Error.url))
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { // session’s datatask here. let data = Data() do {
let models = try JSONDecoder().decode(
decodeType,
from: data
)
completionHandler(.success([models])) } catch let error {
completionHandler(.failure(Error.decode(error)))
}
}
}
func fetch<Model>(
withUrl url: URL?,
decodeType: Model.Type,
completionHandler: @escaping (Result<[Model], Error>) -> ()
) where Model: Decodable {
guard let url = url else {
completionHandler(.failure(Error.url))
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
// session’s datatask here.
let data = Data() do {
let models = try JSONDecoder().decode(
decodeType,
from: data
)
completionHandler(.success([models])) } catch let error {
completionHandler(.failure(Error.decode(error)))
}
}
}

As mentioned before, the fetch() methods are called based on an authorization level so let’s create some phantom types.

protocol AuthorizationLevel {
static var accessToken: String { get }
}
struct User: AuthorizationLevel {
static var accessToken: String {
return "some user token"
}
}
struct Manager: AuthorizationLevel {
static var accessToken: String {
return "some manager token"
}
}
struct Admin: AuthorizationLevel {
static var accessToken: String {
return "some admin token"
}
}

Let’s define a Resource generic struct with a URL, a model to parse and an authorization level.

struct Resource<Model: Decodable, Access: AuthorizationLevel> {
let model: Model.Type
let url: URL?
}

Notice as we don’t actually use the Access generic type within our code. What we really need is the concrete type tied to it.

Finally, let’s refactor using the Resource struct in the NetworkManager class.

class NetworkManager {
static let shared = NetworkManager()
private init() {} func fetch<Model>(
resource: Resource<Model, User>,
completionHandler: @escaping (Result<Model, Error>) -> ()
) {
guard let url = resource.url else {
completionHandler(.failure(Error.url))
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
// session’s datatask here.
let data = Data() do {
let model = try JSONDecoder().decode(
resource.model,
from: data
)
completionHandler(.success(model)) } catch let error {
completionHandler(.failure(Error.decode(error)))
}
}
}
func fetch<Model>(
resource: Resource<Model, Manager>,
completionHandler: @escaping (Result<[Model], Error>) -> ()
) {
guard let url = resource.url else {
completionHandler(.failure(Error.url))
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
// session’s datatask here.
let data = Data() do {
let models = try JSONDecoder().decode(
resource.model,
from: data
)
completionHandler(.success([models])) } catch let error {
completionHandler(.failure(Error.decode(error)))
}
}
}
func fetch<Model>(
resource: Resource<Model, Admin>,
completionHandler: @escaping (Result<[Model], Error>) -> ()
) {
guard let url = resource.url else {
completionHandler(.failure(Error.url))
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
// session’s datatask here.

let data = Data()
do {
let models = try JSONDecoder().decode(
resource.model,
from: data
)
completionHandler(.success([models])) } catch let error {
completionHandler(.failure(Error.decode(error)))
}
}
}
}
struct DataModel: Decodable {
let property: String
}
let resource = Resource<DataModel, User>(
model: DataModel.self,
url: URL(string: “”)!
)
NetworkManager.shared.fetch(resource: resource) { result in
guard let model = try? result.get() else {
return
}
// do something with model
}

The use of phantom types provides a safety net as we ensure at compile time that the fetch() method with the right authorization level (e.g ‘User‘) will be called.

The use of generic « types that can work with any type » is very powerful when it comes to writing reusable code while allowing specialization.

Swift, as a type-safe language, will always give us a warning whenever a generic doesn’t have a tied concrete type.

Generics, combined with type constraints, gives the ability to rule out any sort of confusion or code complexities while providing a clear, well-defined context.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store