Unlocking the Power of Swift Generics: A Comprehensive Guide for Developers

Unlocking the Power of Swift Generics: A Comprehensive Guide for Developers

iOSSwift
Fix bugs faster! Log Collection Made Easy
START NOW!

Introduction to Swift generics

Swift is a high-level programming language developed by Apple, which first appeared on June 2, 2014. Swift is vast and complex, containing all the major features we expect in a modern programming language.

Generics are one of the most fundamental tools in all of Swift, empowering us to write more abstract, reusable and clean code. With Generics, we can use different data types in the same functions and classes, with minimum assumptions.

You can find generics everywhere in Swift. In fact, you’re probably already using generic implementations, since they crop up time and again in the Swift standard library. For example, optionals are a generic data type, we can have an optional of any data type. We will also find Generic collections among Swift’s Dictionary and Array types.

Download the complete source code from our Github as a Swift Playgrounds book that contains all the examples. You will be able to play around with them while reading the article.

The benefits of using generics in Swift

The modern software development landscape requires quicker releases and shorter timeframes to meet the demands of product managers and clients.

Swift generics are crucial in this respect. They give us a framework or package to get all similar functions into the same place, which enables us share the same code with other team members. This means better performance, enhanced type safety, and higher code versatility.

Some of the specific benefits include:

  • The development of adaptable and reusable code that works with a variety of types, without compromising safety.
  • Enhanced performance optimization, minimizing the requirement for runtime checks and preventing pointless type conversions.
  • Cleaner and more concise code, eliminating the need for repetitive type-specific implementations. This, in turn, allows us to write more flexible and scalable code that can easily adapt to future changes or additions, without extensive modifications.
  • Better collaboration among team members, through the creation of a clear and standardized way to handle different types within a project.

We can create generic functions and classes to work with various different types of data. A generic function can be used to sort an array of integers, strings, or any other comparable type, without the need to write separate sorting algorithms for each of them. As well as cutting development time, this reduces the risk of introducing inconsistencies and bugs.

Swift generics also facilitate the creation of generic data structures, such as linked lists, stacks, and queues, which can be utilized in various scenarios and can be easily integrated into different projects, facilitating the creation of reusable components.

For instance, we can write a simple function that takes two arguments, both integer and floating-point, and returns the correct result.

func sum<T: Numeric>(_ a:T, _ b:T) -> T {
    return a + b
}

let resultOfInteger: Int = sum(10, 10)
print("Result of Integer: \\(resultOfInteger)")

let resultOfDecimal: Float = sum(10.5, 10.2)
print("Result of Decimal: \\(resultOfDecimal)")

In this code, <T: Numeric> must be a type that conforms to Numeric protocols. Function arguments a and b can be anything that conforms to Numeric protocols; the function returns a different type either an Int or a Float value.

Let’s copy the code, paste it into our playgrounds and run it.

As the above example shows, Swift generics allow us to use a single function for different types of calculations, eliminating the need to write multiple functions. Thus, by utilizing Swift generics, we can tackle complex problems in a more straightforward way.

You can even use Swift generics with Swift closures to make it even more powerful and provide a very high level of abstraction.

func sum<T: Numeric>(_ a: T, _ b: T, operation: (T) -> T) -> T {
    let result = a + b
    return operation(result)
}

let resultOfInteger = sum(10, 10) { result in
    // Perform some additional operation. In this simple example, just return the result.
    return result
}
print("Result of Integer: \(resultOfInteger)")

let resultOfDecimal = sum(10.5, 10.2) { result in
    // For example, you might want to round the result in the closure.
    return round(result)
}
print("Result of Decimal: \(resultOfDecimal)")

In this example we take advantage of the generic function we have created before adding a closure that makes the function flexible, as you can decide what additional processing to do with the sum each time you call the function. For example, you might just return the sum as it is, or you could round it off, multiply it, etc., depending on what you define in the closure.

Swift generics in detail

Now we’ve outlined the benefits of Swift generics, we’re going to dig deeper into their use-cases and real-world application in modern app development. Along the way, we’ll talk about generic functions, types, extending generics, type constraints and associated types, among other things.

Generic types and type parameters

In Swift, we can do way more than write generic functions. We can also make our own generic types, such as custom classes, structures, and enumerations, and these can be used with any type we like.

In this section, we will create a stack that can work with different types of images. For example, we will create three structs for PNG, GIF and JPEG images, with specific stack functionality for each of them.

struct PNG {
    init() {
        print("A PNG")
    }
}

struct GIF {
    init() {
        print("A GIF")
    }
}

struct JPEG {
    init() {
        print("A JPEG")
    }
}

First, let’s take a look at the non-generic version. This struct can only take our PNG type; no other type can be used here.

This structure uses an array called items to store the values in the stack. Stack provides two methods, push and pop, and these are marked as mutating since we need to modify the struct’s item property, which is an array.

The lack of support for non-PNG images is a limitation here. It would be helpful to have a generic stack that can handle different types of images.

struct Stack {
    var items: [PNG] = []
    mutating func push(_ item: PNG) {
        items.append(item)
    }
    mutating func pop() -> PNG {
        return items.removeLast()
    }
}

For the generic version, we can use a type parameter called element and later pass anything in its place. For this, we will create a generic class called stack, which will keep all types of images like PNG, GIF, and JPEG.

In the below code, we use AnyImage as a placeholder name for a type that we will provide later. This future type can be referred to as an element anywhere within the structure’s definition.

Note that AnyImage is used as a generic type, instead of an image type. We will also create a property called items, which is initialized with an empty array of values of type AnyImage. We also have two mutating functions to add and remove an item in the items array.

struct Stack<AnyImage> {
    var items: [AnyImage] = []
    
    mutating func push(_ item: AnyImage) {
        items.append(item)
    }
    
    mutating func pop(_ item: AnyImage) {
        items.removeLast()
    }
    
    func printAll() {
        for i in items {
            print(i)
        }
    }
}

var imageHolder: Stack = Stack<Any>()

imageHolder.push(PNG())
imageHolder.push(GIF())
imageHolder.push(JPEG())

print("==============Stack Push============")
imageHolder.printAll()

imageHolder.pop(JPEG())
print("==============Stack Pop============")
imageHolder.printAll()

Let’s run the above code on our Playgrounds and hit the run button. The stack works perfectly with all types of images; because it’s a generic type, we create a new Stack instance by writing the type to be stored in the stack within angular brackets.

For example, to create a new string stack, we need the following code:

var stringHolder: Stack = Stack<String>()
stringHolder.push("String")
stringHolder.printAll()

The above two cases show the power of Swift generics. As you’ll see, our stack works with any image type and also with a string.

Associated types and type constraints

Associated types are simply placeholders in a protocol. When we conform to the protocol, we must replace them with concrete types. Alternatively, we can name them generic protocols.

A protocol can have multiple associated types, and these provide flexibility for conforming types to decide which types to use in place of each associated placeholder.

For example, let’s say I want to build a ListView that works with any kind of API and data type that the API delivers to us.

First, let’s create our data model. TypeOne will conform to the protocol MyType.

protocol MyType {
    var name: String { get }
}

class TypeOne: MyType {
    var id: Int
    var name: String
    
    init(id: Int, name: String) {
        self.id = id
        self.name = name
    }
}

class TypeTwo: TypeOne {
    var color: String = "red"
}

Now, create two simple APIs that conform to our protocol listing. The listing protocol has an associated type list, which we will replace with the concrete types TypeOne in ApiOne and TypeTwo in ApiTwo.

protocol Listing {
    associatedtype List
    func get() -> [List];
}

class ApiOne: Listing {
    typealias List = TypeOne
    func get() -> [List] {
        return [List(id: 1, name: "One")]
    }
}

class ApiTwo: Listing {
    typealias List = TypeTwo
    func get() -> [List] {
        return [List(id: 2, name: "Two")]
    }
}

Finally, our ListView accepts any type that conforms to the protocol Listing.

class ListView {
    private var api: (any Listing)
    
    init(api: (any Listing)) {
        self.api = api
        
        for i in self.api.get() {
            print((i as! MyType).name)
        }
    }
}

Now we can simply inject two APIs into our ListViews.

let listViewOne = ListView(api: ApiOne())
let listViewTwo = ListView(api: ApiTwo())

The  Listing protocol’s associative list can work with any type. However, it’s sometimes useful to enforce certain type constraints on those that can be used with associative types.

Type constraints specify that a type must inherit from a specific class, or conform to a particular protocol (or protocol composition). We can simply apply the constraint by adding a data type to associatedtype List: MyType.

Now, when we conform to the protocol, we must use any class that conforms to the MyType protocol. In our case, we already followed the constraint.

protocol Listing {
    associatedtype List: MyType
    func get() -> [List];
}

Generic functions and function signatures

We already wrote our generic function in this tutorial; with sum, we can see how to use generics in Swift. However, the signature of the generic function is a little complex to see.

In the code below, we have declared a function called sum that takes two arguments of generic type T and returns the same generic type T.

func sum<T: Numeric>(_ a:T, _ b:T) -> T {
    return a + b
}

let resultOfInteger: Int = sum(10, 10)
print("Result of Integer: \\(resultOfInteger)")

let resultOfDecimal: Float = sum(10.5, 10.2)
print("Result of Decimal: \\(resultOfDecimal)")

In the function sum() example, the placeholder type T is an example of a type parameter. These parameters specify and name a placeholder type, which is written immediately after the function’s name between two matching angle brackets.

Type inference and type constraints

In Swift, we can use type inference to infer any type of variable or constant. With this function, we can declare a constant or variable without clearly stating what data type it is.

For example, if we declare a string variable, we writehello = "World". Based on the value assigned to it, Swift will automatically know the type to beString-based.

Using generic type constraints, we can impose requirements on concrete types in any class or struct, allowing assumptions in generic code.

For instance, let’s suppose an app we’re developing utilizes a APIRequest protocol to define different request types and their expected Response.

protocol APIRequest { 
	associatedtype Response: Codable 
	
	func get() -> URLRequest 
}

Let’s define an API to execute this request. This might involve a function that uses a generic type constraint, to guarantee that T satisfies our protocol.

func load<T: APIRequest>( _ request: T, onComplete: @escaping (Result<T.Response, Error>) -> Void ) {
																							
}

Real-world use cases of Swift generics

Ok, now let’s imagine we’ve got an e-commerce app, with a cart option where we can add or remove any product before going to final checkout.

However, the challenge arises when we want to implement a feature that allows users to apply different types of products to their cart items. This requires careful consideration of the product categories and their compatibility with the cart items.

Our product can be a general product, or a bundle product. By implementing generics, we can solve this type of problem in the application. In our example, the cart class can handle any class that conforms to theCartable protocol.

We can write Cartable as a simple protocol with three properties: name, quantity and price. Our Product class conforms to the Cartable protocol and BundleProduct inherits to Product class. This allows the cart class to handle both Product and BundleProduct objects.

Finally, we can add or remove any item in our Cart and get the details, like quantity and price. This allows the cart class to handle both Product and BundleProduct objects, ensuring compatibility and flexibility when managing different types of items.

protocol Cartable {
    var name: String { get set}
    var quantity: Int { get set }
    var price: Double { get set }
}

class Product: Cartable {
    var quantity: Int
    var price: Double
    var name: String
    
    init(name:String, price: Double, quantity: Int) {
        self.name = name
        self.price = price
        self.quantity = quantity
    }
}

class BundleProduct: Product {
    var numberOfItem: Int
    
    init(numberOfItem: Int, name:String, pricePerItem: Double, quantity: Int) {
        self.numberOfItem = numberOfItem;
        super.init(name: "Bundle of \\(self.numberOfItem)Pcs \\(name)", price: pricePerItem, quantity: quantity * self.numberOfItem)
    }
}

class Cart<T: Cartable> {
    var items: [T] = []
    
    func add(_ item: T) {
       items.append(item)
    }

    func remove(_ item: T) {
        items.removeLast()
    }
    
    func allItems() {
        print("Name \\t Quantity \\t Price \\t Total")
       for i in items {
        print("\\(i.name) \\t  \\(i.quantity) \\t \\(i.price) \\t \\(i.price * Double(i.quantity))")
       }
        print("\\n")
   }
    
    func totalPrice() -> Double {
        var t: Double = 0.0
        
        for i in self.items {
            t += i.price * Double(i.quantity)
        }
        
        return t
    }
    
    func totalItems()-> Int {
        return items.count
    }
}

var cart: Cart = Cart<Product>()
cart.add(Product(name: "Sweat Shirt", price: 15.99, quantity: 10))
cart.add(Product(name: "Samsung OLED", price: 1400.00, quantity: 1))
cart.add(BundleProduct(numberOfItem: 10, name: "Pants ", pricePerItem: 50.00, quantity: 1))

cart.allItems()
print("Cart Quantity: \\(cart.totalItems())")
print("Cart Total Price: $\\(cart.totalPrice())")

Best practices and guidelines for writing generic code in Swift

Generics are extremely versatile and practical, but we still need to remember a handful of best practices and guidelines to get the most out of them.

These include using clear and descriptive variable and function names, organizing code into logical modules and classes, using optionals and error handling to handle unexpected situations, and following the Don’t Repeat Yourself (DRY) principle to avoid code duplication.

Following these guidelines will ensure that our Swift code is easy to understand, maintain, and debug, leading to more efficient and error-free development.

Naming

Programs are much easier to read and comprehend when their names are descriptive and consistent. So be sure to refer to the API Design Guidelines for information on the proper way to name Swift Generics and other code elements.

Use type-inferred context

Create more readable and concise code by making use of compiler-inferred context. This allows the compiler to infer the type of a variable or expression, based on its surrounding context.

Naming parameters

Be sure to provide descriptive, uppercase names for generic type parameters. Whenever a type name isn’t associated with anything significant, just use a standard capital letter like T, E, or V.

Language

To conform to Apple’s API, it’s best to use U.S. English spelling.

Code Organization

Code can be organized into logical blocks of functionality with the help of extensions, which allow us to add new functionality to an existing type.

Protocol conformance

It is advisable to include a separate extension for the protocol methods when incorporating protocol conformity into a model. This will make the code more readable and maintainable.

Additionally, separating the protocol methods into their own extension allows for easier navigation and understanding of the code structure.

Unused code

Dead code, such as Xcode template code and comments used as placeholders, should be deleted. The only time this rule doesn’t apply is if our book or lesson specifically says to use the commented code.

In all other cases, unused code should be removed to improve code-cleanliness and reduce potential confusion.

Testing

Generics add a layer of abstraction and flexibility to your code which can help you on the development process, as it allows you to write more reusable and adaptable code. However, this abstraction has a price as it also introduces complexity and that’s why it’s important to write proper unit tests to you generic code. It’s important to test that your generic code works correctly with different types as intended and to ensure that the code behave consistently across various types.

Type erasure and opaque types

In addition to the tips and techniques outlined above, it is important that we know how to use type erasure and opaque types. In Swift, this refers to the concept of hiding the specific type of a generic object and treating it as a more general type.

The most straightforward way to think of type erasure is to consider it a way to hide an object’s “real” type. Some examples that come to mind are Combine’s AnyCancellable and AnyPublisher.

For example, the following example runs just fine since protocol Vehicle is not constrained, while the compiler easily allows us to refer the Car and Vehicle by their bare conformity. However, when using protocols with associated types, things can get trickier.

protocol Vehicle {}

struct Car: Vehicle {}

struct Bus: Vehicle {}

var vehicles = [Vehicle]()

vehicles.append(Car())
vehicles.append(Bus())

Let’s try another example. Here, we can’t use var eVehicles = [ElectricVehicle]() . The compiler shows an error, even though it has no problem determining the underlying type of the ElectricVehicle type.

var eVehicles = [ElectricVehicle]()

Before Swift 5.7 we could use Any instead of the ElectricVehicle type. This process, called type erasure Swift standard library, contains many such classes itself.

protocol ElectricVehicle {
    associatedtype Element
}

class ElectricCar: ElectricVehicle {
    typealias Element = ElectricCar
}

var eVehicles = [Any]()
eVehicles.append(ElectricCar())

In Swift 5.7 this has changed. Now, we can use the any type syntax to erase the type of a generic parameter. This provides more flexibility and cleaner code when working with generic types.

var anyEVehicles: any ElectricVehicle = ElectricCar()

This line declares a variable anyEVehicles with the type any ElectricVehicle. This means anyEVehicles can hold any object that conforms to the ElectricVehicle protocol.

Using opaque types to hide implementation details in generic code

Opaque types and boxed protocol types are two methods provided by Swift to hide the type information of a value. It is helpful to hide the return type value at the boundary between a module and the code that calls it; this way, the underlying type of the return value can remain a secret.

By returning an opaque type, a function or method conceals the type information of its return value. The return value is defined in terms of the protocols it supports rather than a concrete type, which is the default for function return types. Although the compiler has access to the type information, clients of the module do not. Opaque types retain type identity.

An opaque type is the polar opposite of a generic type. With generic types, the code calling a function can choose the type of the parameters and return value without having to worry about how the method is really implemented. The following code snippet demonstrates how a function can return a type that is dependent on the caller.

For example, our func start() easily hides the implementation details from the caller. We know func start returns some form of vehicle, but we don’t know what type of vehicle.

protocol Vehicle {
    
}

class Car: Vehicle {
    
}

func start() -> some Vehicle {
    return Car()
}

start()

Difference between opaque and boxed types

While an opaque type and a boxed protocol type appear to be identical, the question of whether or not they maintain type identity distinguishes the two.

In contrast to opaque types, which only the function’s caller can see, boxed protocol types can refer to any type that satisfies the protocol’s requirements.

To sum up

In this lesson, we’ve covered a lot of found: we’ve learned about advanced generics, associated types, and generic constraints in Swift. Mastering these concepts will enable us to design code that is very versatile and reusable, meeting the demands of various scenarios and also how can it help in functional programming. It will allow you also to be ready for the constant Swift evolution.

Generics allow us to write concise, efficient, and type-safe code in Swift, which makes projects more scalable and easier to maintain. With the knowledge we’ve gained from this article, we’re ideally placed to start utilizing generics to elevate our programming to the next level.

Expect the Unexpected! Debug Faster with Bugfender
START FOR FREE

Trusted By

/assets/images/svg/customers/cool/ubiquiti.svg/assets/images/svg/customers/highprofile/gls.svg/assets/images/svg/customers/projects/taxify.svg/assets/images/svg/customers/highprofile/kohler.svg/assets/images/svg/customers/projects/sk_telecom.svg/assets/images/svg/customers/cool/websummit.svg/assets/images/svg/customers/highprofile/intel.svg/assets/images/svg/customers/highprofile/schneider_electric.svg

Already Trusted by Thousands

Bugfender is the best remote logger for mobile and web apps.

Get Started for Free, No Credit Card Required