This article we will learn about a number of design patterns useful in iOS.
Strategy
Strategy pattern allows to select an algorithm at runtime. Instead of executing single algorithms directly, the code will identify each type via the initialization function and then perform the corresponding action.
We often use different tonas that only care about runtime but don't care about how to implement them.
Simple example, the algorithm needs to know how to fly
. Class Duck knows how to "fly" and so does Rocket class. Our algorithm does not care if done, it will need a pair of wings or gas, just need to know if they can fly or not.
1 | protocol Fly { func fly() } class Duck: Fly { func fly() { print("spread wings") } } class Rocket: Fly { func fly() { print("vrooommm!!") } } let flyableObject: Fly = Rocket() flyableObject.fly() |
The problem is solved when using the strategy
In this project we need to get the viewController but we also need them to have some predefined behavior. This, Strategy pattern can do very well. A generic ViewController in mobile app is for Login. So just create a protocol to determine what actions LoginViewController has. This means that the LoginViewController is in the project but we don't need to be mindful about the UI if it has a tableView or animation or some validation method. But we need it to know how to do login.
1 | protocol LoginViewControllerActions { func loginBtnPressed(user: User) } //swift 3 protocol LoginViewController: UIViewController { let user: User var delegate: LoginViewControllerActions? } |
Factory
Factory Method Pattern solves the problem of creating objects without knowing how the object will be created.
Factory Pattern is useful when you create objects at runtime. So if the user wants a cheese pizza you will create CheesePizza (), if you want peperoni, it will create PeperoniPizza ().
1 | enum PizzaType { case cheese case pepperoni case greek } class PizzaFactory { func build(type: PizzaType) -> Pizza { switch type { case cheese: return CheesePizza() case pepperoni: return PepperoniPizza() case greek: return GreekPizza() } } } |
The problem is solved when using the factory
In a similar project, you use a factory pattern to create objects at runtime by passing them what they need.
We need to pass a LoginViewController which can be created using different approaches like Storyboards, xib or coded. We have a factory instance, with an input parameter that can change the way we create that object.
1 | protocol LoginViewControllerActions { func loginBtnPressed(user: User) } //swift 3 protocol LoginViewController: UIViewController { let user: User var delegate: LoginViewControllerActions? } protocol LoginViewControllerFactory { func build(delegate: LoginViewControllerActions) -> LoginViewController } class ViewCodedLoginViewControllerFactory: LoginViewControllerFactory { func build(delegate: LoginViewControllerActions) -> LoginViewController { return ViewCodedLoginViewController(delegate: delegate) } } class StoryboardLoginViewControllerFactory: LoginViewControllerFactory { func build(delegate: LoginViewControllerActions) -> LoginViewController { let viewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("LoginViewController") as LoginViewController viewController.delegate = delegate return viewController } } |
With Factory instance, we only need to call the build
method. This factory can be instance of ViewCodedLoginViewControllerFactory or StoryboardLoginViewControllerFactory we don't really care about, just need to implement the build
method and return a LoginViewController object.
1 | let viewController = factory.build(delegate: self) //LoginViewControllerFactory self.presentViewController(viewController, animated: false, completion: nil) |
Decorator
The decorator pattern allows behavior to be added to a single, static, or dynamic object, without affecting the behavior of other objects in the same class.
A simple example, Coffee shop wants to add whip cream to coffee and calculate new prices and describe based on drinks.
1 | protocol Beverage { func cost() -> Double func description() -> String } class Coffee: Beverage { func cost() -> Double { return 0.95 } func description() -> String { return "Coffe" } } class Whip: Beverage { let beverage: Beverage init(beverage: Beverage) { self.beverage = beverage } func cost() -> Double { return 0.45 + self.beverage.cost() } func description() -> String { return self.beverage.description() + ", Whip" } } var darkRoast: Beverage = Coffee() darkRoast = Whip(beverage: darkRoast) darkRoast.description() darkRoast.cost() // Như vậy darkRoast đã được tính thêm giá của cà phê thêm whip |
The problem is solved when using decorator
We need different API version for each service call, this may have different ways to handle but we use this pattern to hteme a custom for Request Header. With this approach, we can just finish the current job, but in the future, it is ok to add another API call to the header.
1 | public typealias JsonObject = [String : Any] public protocol Request { func request(method: HTTPMethod, data: JsonObject, header: JsonObject?, completion: @escaping (Result) -> Void) } public class MyRequest: Request { public init() { } public func request(method: HTTPMethod, data: JsonObject, header: JsonObject?, completion: @escaping (Result) -> Void) { //do request } } public class MyHeader: Request { let request: Request let header: [String: String] public init(request: Request, apiVersion: APIVersion = .standard){ self.request = request self.header = ["myapikey": "apiKey", "key" : "key", "version" : "(apiVersion.rawValue)"] } public func request(method: HTTPMethod, data: JsonObject, header: JsonObject?, completion: @escaping (Result) -> Void) { let mutableHeader = self.header + (header ?? ) self.request.request(method: method, data: data, header: mutableHeader, completion: completion) } } let v1Request: Request = MyHeader(request: MyRequest(), apiVersion: .v1) let standardRequest: Request = MyHeader(request: MyRequest()) |
We also need to filter the result of the request. What can be done when creating a new method, or changing your request. We decided to use Decorator to add this behavior because our service has been used in another class and we want to change at least some of the least possible lines.
1 | protocol Service { func fetch(completion: @escaping (Result<[String]>) -> Void) -> Void } class ViewControllerLoader<D> { func load(completion: @escaping (Result<D>) -> Void) { fatalError("load method need to be override on subclasses") } } class ServiceViewControllerLoader: ViewControllerLoader<[String]> { let service: Service init(service: Service) { self.service = service } override func load(completion: @escaping (Result<[String]>) -> Void) { self.service.fetch() { (result) in switch result { case .success(let strings): completion(.success(strings)) case .error(let error): completion(.error(error)) } } } } class ServiceViewControllerLoaderDecorator: ViewControllerLoader<[String]> { let loader: ViewControllerLoader<[String]> init(loader: ViewControllerLoader<[String]>) { self.loader = loader } func filter(data: [String]) { //do filtering } override func load(completion: @escaping (Result<[String]>) -> Void) { self.loader.service.fetch { (result) in switch result { case .success(let strings): let filteredStrings = self.filter(data: strings) completion(.success(filteredStrings)) case .error(let error): completion(.error(error)) } } } } |
Adapter
Adapter pattern is a software design pattern that allows the interface of the existing class to be used as another interface. It is often used to create an existing class that works with another class without modifying the source code
Let's think of an adapter as a practical converter. Suppose you need Nintendo 64 to have a composite video to be the output for your 4k TV. Therefore you need a Composite-HDMI converter.
The problem is solved when using an adapter
You need the last 4 digits of a Card and are also compatible with PKPaymentPass. In other words, create an adapter to return a PKPaymentPass as an instance of the Card.
1 | public struct Card { var lastNumber: String = "" } public struct PassKitCard { let passKitCard: PKPaymentPass? public func toCard() -> Card { return Card(lastNumber: paymentPass.primaryAccountNumberSuffix) } } |
But we also need to mix patterns to create reusable code and maintainability. For example, mix adapter with strategy pattern. We don't really need a Card object, we just need to lastNumbers so why don't we create a protocol to implement and integrate PKPaymentPass, Card and any other Objects you may need.
1 | public protocol LastNumber { var lastNumber: String { get } } public struct PassKitLastNumber: LastNumber { let passKitCard: PKPaymentPass? public var lastNumber: String { if let paymentPass = self.passKitCard { return paymentPass.primaryAccountNumberSuffix } return "" } } class Card: LastNumber { let card: Card init(card: Card) { self.card = card } var lastNumber: String { return self.card.lastNumbers } } |
summary
Design patterns help us a lot in reusing code, saving time when changing some features in code and some tasks are also easy to use.
Link to reference: https://medium.com/cocoaacademymag/real-world-ios-design-patterns-3e5aad172094