Back to Blogs

Swift I: Is Generics the quarterback of Swift programming?

iOS App Development
Article
Technology, Information and Media
What is Generics, and why is it garnering so much attention?

Generics is a type of code that allows developers to write reusable and flexible functions that can be used with various data types. Moreover, it is one of the key features of Swift, Apple’s programming language that is used to develop iOS, Mac, Apple TV, and Apple Watch apps. 

Introduction to Generics in Swift

So, what’s the biggest advantage that Generics offers Swift? Well, the ability to write abstract code jumps to mind. Why is this important? That’s because code abstraction is an essential component of object-oriented programming. 

Abstraction is the process of hiding the background parts or implementation details from users. This way, users can only see the required information. For example, a user operating an oven only needs to know how to handle the oven's controls and doesn't need to know anything about the internal working of the oven.

This abstraction in programming languages is achieved by defining the access modifiers like public or private and creating abstract classes. In the case of Swift, protocols can be used instead. 

A protocol in Swift is a blueprint of methods, properties, and other requirements that suit a particular functionality. Such protocols can be adopted by types such as class, structure, or enumeration. This helps implement those requirements and is known as conforming to the protocol. 

Another crucial element provided by Generics in Swift is polymorphism, which is also one of the principles of object-oriented programming. So, let’s dive into the world of polymorphism with some pizza 🍕

Polymorphism and its types

In an object-oriented system, developers often work with instances of various types that share characteristics. This occurs due to classes or subclasses inheriting from the same ancestor. 

For example, if an object ‘car’ exists, it could have numerous subclasses such as sedan, hatchback, and SUV. 

Here is where polymorphism enters the frame and provides an interface to entities of different types. Now, let's take a look at the types of polymorphism with the example of a pizza shop (we did promise you some pizza, didn't we 😉)

Subtype polymorphism

This type of polymorphism takes place when a parent type of a high-level code is created, and the detailed low-level code in the child type conforms to the parent type. 


import Foundation

enum PizzaTopping: String {
    case onion = "Onion"
    case capsicum = "Capsicum"
    case corn = "Corn"
    case cheese = "Cheese"
    case lotsOfCheese = "Lots of cheese"
}

class Pizza {
    var toppings: [PizzaTopping] = []
    func preparePizza(){
        print("Pizza is ready!")
    }
}

class Margherita: Pizza {
    override func preparePizza() {
        self.toppings.append(.lotsOfCheese)
        print("Margherita Pizza is ready!")
    }
}

In this example, we have created a parent class ‘Pizza’, which has a type called toppings and a preparePizza(). To create a detailed product in the case of the pizza shop, we create a subclass Margherita that conforms to Pizza class and overrides the preparePizza() method. In subtype polymorphism, a new subclass has to be created for each type of pizza.


//Usage
let newPizza = Margherita()
newPizza.preparePizza()

Users can then create new instances of pizzas they want and implement the preparePizza() method on them.

Ad-hoc polymorphism

This form of polymorphism occurs when method overloading is implemented. Method overloading refers to a scenario where a class has numerous methods with the same name but differ in parameters.

Continuing with the pizza example, we have to set up overloaded methods to create different types of pizza. Furthermore, we also need to create a new overloaded method for each pizza type.


import Foundation

enum PizzaTopping: String {
    case onion = "Onion"
    case capsicum = "Capsicum"
    case corn = "Corn"
    case cheese = "Cheese"
    case lotsOfCheese = "Lots of cheese"
}

class Pizza {
    var toppings: [PizzaTopping] = []
    func preparePizza() {
        self.toppings = [.lotsOfCheese]
        print("Margherita Pizza is ready!")
    }
    func preparePizza(with toppings: [PizzaTopping]) {
        self.toppings = toppings
        print("Customized Pizza is ready!")
    }
}

Here, we are creating two types of pizzas—Margherita and customized pizza. So, we create a class called Pizza with a type for toppings and overloaded the preparePizza methods for making different pizzas.


//Usage
let newOrder = Pizza()
newOrder.preparePizza()
let newOrder2 = Pizza()
newOrder2.preparePizza(with: [.capsicum, .cheese])

Users can now create an instance of the pizza using the appropriate method for the pizza that they want. 

Parametric polymorphism

Parametric polymorphism forms the base of generics and employs the same implementation for different parameter types. Let’s understand this better with our pizza shop example.


import Foundation

enum PizzaTopping: String {
    case onion = "Onion"
    case capsicum = "Capsicum"
    case corn = "Corn"
    case cheese = "Cheese"
    case lotsOfCheese = "Lots of cheese"
}

protocol ItemName {
    var message: String { get }
}

Keeping the same pizza shop example, here we created the same PizzaTopping enum with a case for each topping. We also created a protocol ItemName that only has one type called string. 


enum PizzaType: String, ItemName {
    case margherita = "Margherita"
    case farmhouse = "Farmhouse"
    case goldenCorn = "Golden corn"
    
    var toppings: [PizzaTopping] {
        switch self {
        case .margherita :
            return [.lotsOfCheese]
        case .farmhouse:
            return [.corn, .capsicum, .onion, .cheese]
        case .goldenCorn:
            return [.corn, .cheese]
        }
    }
    var message : String {
        "\(self.rawValue) pizza is ready!"
    }
}

We’ll start off by creating a PizzaType that conforms to the ItemName protocol. In this case, we’ll be making three types of pizza—Margherita, farmhouse, and golden corn. We would also be using the array of PizzaToppings for the type toppings. Additionally, a switch case will also be implemented depending on the pizza type for each pizza’s respective toppings array. Finally, we’re also integrating the type message from protocol ItemName, which will print the name of the pizza that is ready.  


enum SideDishType: String, ItemName {
    case fries = "Fries"
    case calzone = "Calzone"
    var message: String {
        "\(self.rawValue) side dish is ready!"
    }
}

Next, we’ll be creating an enum type called SideDishType that also conforms to the ItemName protocol. The cases in this example would be fries and calzones. Also, the type message from the ItemName protocol will return a string value when each of the side dish is ready. 


class PizzaShop {    
    func prepare<T: ItemName>(type: T) {
        print(type.message)
    }
}

As you can see, we have created a class named PizzaShop, which will use the generic prepare method. This method employs a type T, which conforms to the ItemName protocol in angular brackets. This type T will be passed to the method as a parameter. Within this method definition, we’ll print the type.message. Thus, this method can accept any PizzaType or SideDishType and return the item accordingly.


//Usage
//Pizza orders
var myOrder = PizzaShop()
myOrder.prepare(type: PizzaType.margherita)
var myOrder2 = PizzaShop()
myOrder2.prepare(type: PizzaType.farmhouse)
var myOrder3 = PizzaShop()
myOrder3.prepare(type: PizzaType.goldenCorn)
//Side dish orders
var myOrder4 = PizzaShop()
myOrder4.prepare(type: SideDishType.calzone)
var myOrder5 = PizzaShop()
myOrder5.prepare(type: SideDishType.fries)

As you can see, we have created a class named PizzaShop, which will use the generic prepare method. This method employs a type T, which conforms to the ItemName protocol in angular brackets. This type T will be passed to the method as a parameter. Within this method definition, we’ll print the type.message. Thus, this method can accept any PizzaType or SideDishType and return the item accordingly.


//Usage
//Pizza orders
var myOrder = PizzaShop()
myOrder.prepare(type: PizzaType.margherita)
var myOrder2 = PizzaShop()
myOrder2.prepare(type: PizzaType.farmhouse)
var myOrder3 = PizzaShop()
myOrder3.prepare(type: PizzaType.goldenCorn)
//Side dish orders
var myOrder4 = PizzaShop()
myOrder4.prepare(type: SideDishType.calzone)
var myOrder5 = PizzaShop()
myOrder5.prepare(type: SideDishType.fries)

You can now create an instance of PizzaShop and then call the prepare method. Within the type parameter, any PizzaType or SideDishType can be passed and hence the dish will be prepared!

The bottom line

This just highlights the potential of Generics and the value it adds to Swift. Next up, we keep our Swift series going with the exploration of the updates announced to Generics as part of the Swift 5.7 at WWDC 2022. 

So, keep a lookout 😄

Sahil Satralkar

Sahil is an iOS engineer who constantly strives to improve his coding skills while exploring Swift programming. He enjoys watching Arsenal play and trying new fast-food joints in his spare time.

More by this author