Fun usage of Generic implementation in Swift

Implementing Generic in Swift

Page content

Most of us know that generic in programming means we able to do something not limited to one specific type but has more wide selection on the same basis. The most common examples are Queue or Stack use cases. Let say we would like to implement an implementation of both problems not limited to one type of data type, we would like to use a Generic way to solve this problem like the code below

 1class Stack<T> {
 2    private var data = [T]()
 3
 4    var count: Int {
 5        return data.count
 6    }
 7
 8    func push(_ item: T) {
 9        data.append(item)
10    }    
11    @discardableResult
12    func pop() -> T? {
13        if !data.isEmpty {
14            return data.popLast()
15        } else {
16            return nil
17        }
18    }
19}

With the code above, the word T can be replaced with anything, and it will refer to the Generic type. Later on, we could use the implementation above like below

1let stack = Stack<Int>()
2stack.push(5) // add 5
3stack.push(7) // add 7
4stack.pop() // return and remove 7

How about if we want to hide the internal function of this method and would like to only know the abstraction of the generic implementation itself by only understand the interface? That’s how the protocol associated type comes in. It is quite handy. Protocols with associated types are one of the most powerful, expressive, and flexible features of Swift, but there is some complexity behind it and we may need to control their complexity by adding constraints using them, which restricts how they are used.

USE CASE

Okay now, let’s go-to real use case implementation. Now let say we have a modular architecture, and we would like to have a Factory class that able to generate the correct view output to be present on the screen. Let say we have 3 classes that will work together on a screen, which is Cart , Banner , and Recommendation . These 3 classes will be implemented inside a big ViewController and work on different modules. The easiest way is to make an Engine class on each module to return the correct View to the main app. What is the hassle here? You need to create each Engine for each module! Why don’t we just have one Generic Engine class that able to regenerate each necessary module base on the input and output we need? First, let’s define the protocol type of this generic :

1protocol GenericEngine {
2  associatedtype EngineType
3  associatedtype Output
4  func buildEngine() -> Output
5}

The above protocol will define the interface of our Generic engine which is accepting an Input and Output. Both of these are not being defined yet! That’s mean it is up to you how to define it explicitly later on the class implementation by using typealias or just make it implicitly base on the user input and let the compiler predict it base on the flow or constraint you add.

Finally, let’s create a Generic class base on this protocol. I combine some of the ways into one generic class.

 1final class GenericImplementation<T, V>: GenericEngine {
 2typealias EngineType = T
 3  typealias Output = V
 4
 5  private lazy var factory: EngineFactory = EngineFactory<EngineType, Output>()
 6
 7  func buildEngine() -> Output {
 8     let engine = factory.buildModule()
 9     let action = engine.action
10     print("Got engine type :")
11     action.printType()
12     if let vc = engine.viewController as? Output {
13        return vc
14     } else {
15        fatalError("Ask wrong output type detected")
16     }
17  }
18}

Look at the above class, I have defined a GenericImplementation class that conforms to a GenericEngine, however this Generic class accepting both T and V type inference. T in here is a generic type without any constraints, and base on the protocol of GenericEngine, it can be the EngineType, which means this EngineType can be anything, the same applies to the Output. We can actually explicitly tell what we want to refer with typealias there directly, however, if we declare it in this class, it won’t be a generic one as intended. The last one is the buildEngine implementation we need to make an implementation of a class that can accept the EngineType parameters and produces an Output. However, in the implementation above, we also can validate the output if it is the correct type or not, if not then we need to let the compiler check it during runtime. Let see the rest of the class such as the factory class below

 1final class EngineFactory<Input, Output> {
 2   func buildModule() -> (viewController: UIViewController, action: ModuleAction) {
 3      let moduleAction: ModuleAction
 4      if Input.self == Cart.self {
 5        moduleAction = Cart()
 6      } else if Input.self == Banner.self {
 7        moduleAction = Banner()
 8      } else if Input.self == Recommendation.self {
 9        moduleAction = Recommendation()
10      } else {
11        moduleAction = Recommendation()
12      }
13      return (moduleAction.viewController, moduleAction)
14    }
15}
16
17protocol ModuleAction {
18   var viewController: UIViewController { get }
19   func printType()
20}
21extension ModuleAction {
22   func printType() {
23     print("This one is \(type(of: self))")
24   }
25}
26class Cart: ModuleAction {
27   var viewController: UIViewController {
28     return CartVC()
29  }
30}
31class Banner: ModuleAction {
32   var viewController: UIViewController {
33     return BannerVC()
34   }
35}
36class Recommendation: ModuleAction {
37   var viewController: UIViewController {
38     return RecommendationVC()
39   }
40}
41class CartVC: UIViewController { }
42class BannerVC: UIViewController { }
43class RecommendationVC: UIViewController { }

So the above class is the implementation of our Factory class to produce the correct Module type and Output type. Now how do we use this generic class? See below implementation for the details

1let generic: GenericImplementation<Cart, CartVC> = GenericImplementation()
2let module = generic.buildEngine()
3print("This vc type is \(type(of: module))")

Run the code above and feel free to change your engine type and output type and you can see when it is going to be correct and when is it going to be the wrong one. Happy coding!