Skip to main content

23 questions tagged with "Design Patterns"

Design Patterns tag description

View All Tags

Explain the Decorator Pattern in iOS

· 3 min read
Szymon Michalak
iOS Developer
Sources & Resources
TL/DR

The Decorator Pattern allows you to add functionality to an object dynamically without modifying its code. It uses composition to wrap an object with additional behavior.

Concept Overview​

The Decorator Pattern is a structural design pattern that dynamically modifies or enhances the behavior of an object. Instead of extending a class with inheritance, the pattern relies on wrapping the object and delegating its behavior to add new functionality.

Key components:

  1. Component Protocol: Declares the interface for objects that can be dynamically modified.
  2. Concrete Component: The class implementing the component protocol (the object to be enhanced).
  3. Decorator Class: Implements the component protocol and wraps the concrete component to add behavior dynamically.

Example in iOS: Adding Functionality to Legacy Code​

Here’s how the Decorator Pattern can enhance a legacy class without altering its original code.

Legacy Class​

class LegacyClass {
func doSomething() -> String {
return "Legacy functionality"
}
}

Component Protocol​

protocol LegacyProtocol {
func doSomething() -> String
}

extension LegacyClass: LegacyProtocol {}

Decorator Class​

class Decorator: LegacyProtocol {
private let wrapped: LegacyProtocol
private let additionalFunctionality: () -> String

init(wrapped: LegacyProtocol, additionalFunctionality: @escaping () -> String) {
self.wrapped = wrapped
self.additionalFunctionality = additionalFunctionality
}

func doSomething() -> String {
let legacyResult = wrapped.doSomething()
let additionalResult = additionalFunctionality()
return "\(legacyResult) with \(additionalResult)"
}
}

Usage​

let legacyInstance: LegacyProtocol = LegacyClass()
let decoratorInstance = Decorator(wrapped: legacyInstance, additionalFunctionality: { "new functionality" })

print(legacyInstance.doSomething()) // Output: "Legacy functionality"
print(decoratorInstance.doSomething()) // Output: "Legacy functionality with new functionality"

How It Works​

  1. Component Protocol: The LegacyProtocol ensures compatibility between the legacy class and the decorator.
  2. Concrete Component: The LegacyClass provides the original functionality.
  3. Decorator: The Decorator wraps LegacyProtocol and dynamically adds new functionality.

Unit Testing​

The Decorator Pattern is easily testable, ensuring that new functionality works as expected without impacting the original behavior.

import XCTest

class DecoratorTests: XCTestCase {
func testDoSomething() {
// Given
let legacyInstance: LegacyProtocol = LegacyClass()
let decoratorInstance = Decorator(wrapped: legacyInstance, additionalFunctionality: { "new functionality" })

// When
let result = decoratorInstance.doSomething()

// Then
XCTAssertEqual(result, "Legacy functionality with new functionality")
}
}

Benefits of the Decorator Pattern​

  1. Extensibility: Add functionality without altering existing code.
  2. Open/Closed Principle: Supports open for extension and closed for modification.
  3. Flexibility: Dynamically compose behaviors without inheritance.

When to Use​

  • When extending functionality without subclassing.
  • When working with legacy code or third-party libraries.
  • To avoid a rigid class hierarchy.

Limitations​

  • Complexity: Can lead to a system of many small objects.
  • Debugging: Wrapped objects can make debugging and tracing behavior challenging.

In Bullets
  • The Decorator Pattern enhances objects dynamically without altering their code.
  • It involves components, concrete implementations, and decorators.
  • Useful for adding new features to legacy code or third-party libraries without breaking existing functionality.

Explain the Adapter Pattern in iOS

· 4 min read
Szymon Michalak
iOS Developer
Sources & Resources

Main Source: Ray Wenderlich - Design Patterns by Tutorials (2019)
Further Reading:

TL/DR

The Adapter Pattern allows incompatible interfaces to work together by creating a bridge between them. It’s commonly used in iOS to integrate third-party libraries or services with the app’s own system by adapting their interface to match the expected one.

Concept Overview​

The Adapter Pattern consists of the following components:

  1. Client: The system or module expecting to use a specific interface.
  2. Adapter: The class that implements the expected interface while internally communicating with the legacy or incompatible interface.
  3. Legacy Object: The existing or third-party class that cannot be modified directly but needs to work with the client.

This pattern is useful when integrating third-party services like payment processors, authentication providers, or APIs, making them work seamlessly within your app.

How Adapter Works:​

  1. Client: Expects a certain interface.
  2. Adapter: Implements the interface and translates requests to the legacy object.
  3. Legacy Object: The existing class that performs the actual work.

Key Benefits:​

  • Compatibility: Allows you to integrate incompatible systems without modifying their source code.
  • Decoupling: Reduces dependencies by separating the client from the legacy object directly.

Playground Example​

Here is an example of using the Adapter Pattern to integrate a third-party authentication service into an app:

import Foundation

// MARK: - Legacy Object
public class GoogleAuthenticator {
public func login(email: String, password: String, completion: @escaping (GoogleUser?, Error?) -> Void) {
let token = "special-token-value"
let user = GoogleUser(email: email, password: password, token: token)
completion(user, nil)
}
}

public struct GoogleUser {
public var email: String
public var password: String
public var token: String
}

// MARK: - New Protocol
public protocol AuthenticationService {
func login(email: String, password: String, success: @escaping (User, Token) -> Void, failure: @escaping (Error?) -> Void)
}

public struct User {
public let email: String
public let password: String
}

public struct Token {
public let value: String
}

// MARK: - Adapter
public class GoogleAuthenticatorAdapter: AuthenticationService {
private let authenticator = GoogleAuthenticator()

public func login(email: String, password: String, success: @escaping (User, Token) -> Void, failure: @escaping (Error?) -> Void) {
authenticator.login(email: email, password: password) { (googleUser, error) in
guard let googleUser = googleUser else {
failure(error)
return
}
let user = User(email: googleUser.email, password: googleUser.password)
let token = Token(value: googleUser.token)
success(user, token)
}
}
}

// Example Usage
let authService: AuthenticationService = GoogleAuthenticatorAdapter()
authService.login(email: "user@example.com", password: "password") { user, token in
print("Logged in as \(user.email) with token \(token.value)")
} failure: { error in
print("Login failed")
}

How It Works:​

  • Adapter: The GoogleAuthenticatorAdapter class implements the AuthenticationService protocol expected by the app.
  • Legacy Object: The GoogleAuthenticator class is used without being modified.
  • Client: The app interacts with the GoogleAuthenticatorAdapter to perform login.

When to Use​

  • Third-Party Integration: When you need to work with third-party services that have incompatible interfaces.
  • Legacy Code: When adapting old code to work with new systems without changing the legacy code.

When to Be Careful​

  • Increased Complexity: Using adapters can make code harder to maintain if overused, so it’s important to evaluate whether direct integration is possible.

In Bullets
  • The Adapter Pattern bridges incompatible interfaces by wrapping legacy objects.
  • Components include the client, adapter, and legacy object.
  • Commonly used for third-party service integration or legacy code adaptation.

Explain the Builder Pattern in iOS

· 4 min read
Szymon Michalak
iOS Developer
Sources & Resources

Main Source: Ray Wenderlich - Design Patterns by Tutorials (2019)
Further Reading:

TL/DR

The Builder Pattern allows for the construction of complex objects by providing step-by-step inputs, instead of requiring all inputs upfront. It is particularly useful when an object needs multiple components or custom configurations.

Concept Overview​

The Builder Pattern consists of three primary components:

  1. Director: Accepts inputs and coordinates with the builder. Often implemented as a controller or helper class.
  2. Product: The complex object to be created, often a class or struct, depending on desired reference semantics.
  3. Builder: Accepts step-by-step inputs to create the product. This is typically a class to allow for reuse by reference.

This pattern is particularly useful for creating complex objects in iOS, such as when building a form or configuring a model with multiple parameters.

How Builder Works:​

  1. Director: Takes the inputs and coordinates with the builder to construct the product.
  2. Builder: Manages the actual creation, handling the addition of parts or customization.
  3. Product: The final, fully-built object, assembled step-by-step by the builder.

Key Benefits:​

  • Customization: Allows for the creation of complex products with various configurations.
  • Flexibility: The builder can construct the product in any order, depending on the director's needs.

Playground Example​

Here is a basic example of the Builder Pattern used to build a hamburger:

import Foundation

// MARK: - Product
public struct Hamburger {
public let meat: Meat
public let sauces: Sauces
public let toppings: Toppings
}

public enum Meat: String {
case beef, chicken, tofu
}

public struct Sauces: OptionSet {
public let rawValue: Int
public static let ketchup = Sauces(rawValue: 1 << 0)
public static let mustard = Sauces(rawValue: 1 << 1)
}

public struct Toppings: OptionSet {
public let rawValue: Int
public static let cheese = Toppings(rawValue: 1 << 0)
public static let lettuce = Toppings(rawValue: 1 << 1)
}

// MARK: - Builder
public class HamburgerBuilder {
public private(set) var meat: Meat = .beef
public private(set) var sauces: Sauces = []
public private(set) var toppings: Toppings = []

public func setMeat(_ meat: Meat) {
self.meat = meat
}

public func addSauces(_ sauce: Sauces) {
sauces.insert(sauce)
}

public func addToppings(_ topping: Toppings) {
toppings.insert(topping)
}

public func build() -> Hamburger {
return Hamburger(meat: meat, sauces: sauces, toppings: toppings)
}
}

// Usage example
let builder = HamburgerBuilder()
builder.setMeat(.chicken)
builder.addSauces(.mustard)
builder.addToppings([.cheese, .lettuce])

let burger = builder.build()
print("Built a \(burger.meat.rawValue) burger with toppings.")

How It Works:​

  • Director: The HamburgerBuilder class acts as the director, collecting inputs and coordinating the creation process.
  • Builder: Handles setting the meat, sauces, and toppings step-by-step before finalizing the product.
  • Product: The Hamburger struct is the final product that is built and returned.

When to Use​

  • Complex Objects: When creating an object that requires multiple configurations or parts, such as forms, game characters, or UI elements.
  • Incremental Construction: When you need the flexibility to construct an object incrementally.

When to Be Careful​

  • Overhead: If the object is simple and doesn’t require step-by-step construction, this pattern might introduce unnecessary complexity.
  • Order of Operations: Ensuring that all necessary components are added before the object is built can require careful handling of logic.

In Bullets
  • The Builder Pattern facilitates step-by-step construction of complex objects.
  • It involves three main components: director, builder, and product.
  • Useful for creating objects with multiple configurations, such as forms or game characters.

Explain the Chain of Responsibility Pattern in iOS

· 3 min read
Szymon Michalak
iOS Developer
Sources & Resources

Main Source: Ray Wenderlich - Design Patterns by Tutorials (2019)
Further Reading:

TL/DR

The Chain of Responsibility Pattern allows multiple objects to handle a request, passing it along a chain until one of the objects handles it. It is useful for processing events where different handlers act based on specific criteria.

Concept Overview​

The Chain of Responsibility Pattern is a behavioral design pattern that decouples the sender of a request from its receiver. Multiple handlers are linked, and each handler decides either to process the request or pass it to the next handler in the chain.

Key components:

  1. Client: Sends the request.
  2. Handler Protocol: Defines the required interface for all handlers.
  3. Concrete Handlers: Each handler processes the request or forwards it to the next handler.

Playground Example​

Below is an example based on a coin validation system:

import Foundation

// MARK: - Handler Protocol
protocol CoinHandlerProtocol {
var next: CoinHandlerProtocol? { get set }
func handleCoinValidation(_ coin: Coin) -> Coin?
}

// MARK: - Coin Model
public class Coin {
public let diameter: Double
public let weight: Double

public init(diameter: Double, weight: Double) {
self.diameter = diameter
self.weight = weight
}
}

// Concrete Coin Types
public class Penny: Coin {
public static let standardDiameter = 19.05
public static let standardWeight = 2.5
}

public class Quarter: Coin {
public static let standardDiameter = 24.26
public static let standardWeight = 5.67
}

// MARK: - Concrete Handler
public class CoinHandler: CoinHandlerProtocol {
public var next: CoinHandlerProtocol?
private let coinType: Coin.Type
private let diameterRange: ClosedRange<Double>
private let weightRange: ClosedRange<Double>

public init(coinType: Coin.Type, diameterVariation: Double = 0.05, weightVariation: Double = 0.05) {
self.coinType = coinType
let standardDiameter = coinType.standardDiameter
self.diameterRange = (1 - diameterVariation) * standardDiameter ... (1 + diameterVariation) * standardDiameter
let standardWeight = coinType.standardWeight
self.weightRange = (1 - weightVariation) * standardWeight ... (1 + weightVariation) * standardWeight
}

public func handleCoinValidation(_ coin: Coin) -> Coin? {
if diameterRange.contains(coin.diameter) && weightRange.contains(coin.weight) {
return coin
}
return next?.handleCoinValidation(coin)
}
}

// Example Usage
let pennyHandler = CoinHandler(coinType: Penny.self)
let quarterHandler = CoinHandler(coinType: Quarter.self)
pennyHandler.next = quarterHandler

let unknownCoin = Coin(diameter: 24.26, weight: 5.67)
if let validatedCoin = pennyHandler.handleCoinValidation(unknownCoin) {
print("Coin validated: \(type(of: validatedCoin))")
} else {
print("Coin not recognized")
}

How It Works:​

  • Client: Sends the unknownCoin to the first handler (pennyHandler).
  • Handlers: Each CoinHandler validates the coin. If it cannot handle the request, it passes the request to the next handler in the chain.

When to Use​

  • Event handling: When multiple objects might handle a request, and the specific handler is not known in advance.
  • Decoupling: When you want to decouple senders from receivers and chain multiple handlers together.

When to Be Careful​

  • Long chains: A long chain of handlers can impact performance and make debugging more complex.

In Bullets
  • The Chain of Responsibility Pattern allows requests to be passed along a chain of handlers until one processes it.
  • It involves a client, handler protocol, and concrete handlers.
  • Useful for event handling and decoupling sender and receiver logic.

Explain the Command Pattern in iOS

· 3 min read
Szymon Michalak
iOS Developer
Sources & Resources

Main Source: Ray Wenderlich - Design Patterns by Tutorials (2019)
Further Reading:

TL/DR

The Command Pattern encapsulates requests or actions into objects. This allows you to parameterize methods, queue operations, and support undo functionality.

Concept Overview​

The Command Pattern is a behavioral design pattern that turns a request or action into a command object, which can then be executed, delayed, or queued. The three primary components are:

  1. Invoker: Stores and executes commands.
  2. Command: Encapsulates the action to be performed.
  3. Receiver: The object that the command is performed on.

This pattern is often used in situations where actions need to be executed at a later time or reversed (for undo functionality).

Playground Example​

Below is an example based on a door control system from the book:

import Foundation

// MARK: - Receiver
public class Door {
public var isOpen = false
}

// MARK: - Command
public class DoorCommand {
public let door: Door
public init(_ door: Door) {
self.door = door
}
public func execute() {}
}

public class OpenCommand: DoorCommand {
public override func execute() {
print("Opening the door...")
door.isOpen = true
}
}

public class CloseCommand: DoorCommand {
public override func execute() {
print("Closing the door...")
door.isOpen = false
}
}

// MARK: - Invoker
public class Doorman {
public let commands: [DoorCommand]
public let door: Door

public init(door: Door) {
let commandCount = arc4random_uniform(10) + 1
self.commands = (0..<commandCount).map { index in
return index % 2 == 0 ? OpenCommand(door) : CloseCommand(door)
}
self.door = door
}

public func execute() {
print("Doorman is executing commands...")
commands.forEach { $0.execute() }
}
}

// Example usage
let door = Door()
let doorman = Doorman(door: door)
doorman.execute()

How It Works:​

  • Receiver: The Door class acts as the receiver that will be opened or closed by the command.
  • Command: The OpenCommand and CloseCommand encapsulate the actions of opening and closing the door.
  • Invoker: The Doorman class stores and executes the commands in sequence.

When to Use​

  • Queuing and executing actions: When you need to queue operations to be performed later, or support undo functionality.
  • Decoupling requesters from executors: When the requester (e.g., user interface) should be decoupled from the object that performs the action.

When to Be Careful​

  • Complex command objects: Be cautious of creating too many command objects, as it can lead to more complex and harder-to-maintain code.

In Bullets
  • The Command Pattern turns actions into objects that can be stored and executed later.
  • It involves three main components: invoker, command, and receiver.
  • Useful for queuing, undo functionality, or decoupling requesters from actions.

Explain the Composite Pattern in iOS

· 3 min read
Szymon Michalak
iOS Developer
Sources & Resources

Main Source: Ray Wenderlich - Design Patterns by Tutorials (2019)
Further Reading:

TL/DR

The Composite Pattern organizes objects into tree-like structures to treat individual objects and collections of objects uniformly. It is useful for representing hierarchical data.

Concept Overview​

The Composite Pattern is a structural design pattern that allows you to treat a group of objects as a single object. The key components are:

  1. Component Protocol: The base protocol that defines the common interface for all objects in the structure.
  2. Leaf: Represents individual objects that do not contain other objects.
  3. Composite: A container that holds multiple components, including leaves and other composites.

This pattern is commonly used for building tree-like structures, such as a file system or a UI hierarchy.

Playground Example​

The following example from the book demonstrates how the Composite Pattern is used in a file system:

import Foundation

// Component Protocol
protocol File {
var name: String { get set }
func open()
}

// Leaf Classes
final class eBook: File {
var name: String
var author: String

init(name: String, author: String) {
self.name = name
self.author = author
}

func open() {
print("Opening \(name) by \(author) in iBooks...\n")
}
}

final class Music: File {
var name: String
var artist: String

init(name: String, artist: String) {
self.name = name
self.artist = artist
}

func open() {
print("Playing \(name) by \(artist) in iTunes...\n")
}
}

// Composite Class
final class Folder: File {
var name: String
lazy var files: [File] = []

init(name: String) {
self.name = name
}

func addFile(file: File) {
self.files.append(file)
}

func open() {
print("Displaying the following files in \(name)...")
for file in files {
print(file.name)
}
print("\n")
}
}

// Example usage:
let psychoKiller = Music(name: "Psycho Killer", artist: "Talking Heads")
let justKids = eBook(name: "Just Kids", author: "Patti Smith")

let documents = Folder(name: "Documents")
let musicFolder = Folder(name: "70s Music")
musicFolder.addFile(file: psychoKiller)
documents.addFile(file: musicFolder)
documents.addFile(file: justKids)

documents.open()

How It Works:​

  • Component Protocol: File is the base protocol that both eBook and Music implement.
  • Leaf: eBook and Music represent individual files that implement the File protocol.
  • Composite: Folder is the composite class that holds other File objects (both leaf and composite types).

When to Use​

  • Tree structures: Use the Composite Pattern when you need to represent objects in a hierarchical tree structure (e.g., a file system or UI elements).
  • Uniform object treatment: When you need to treat both individual objects and groups of objects uniformly.

When to Be Careful​

  • Overcomplicating simple structures: Be mindful of introducing unnecessary complexity if a simpler solution can handle the structure.

In Bullets
  • The Composite Pattern groups objects into tree structures, allowing uniform treatment of individual and composite objects.
  • It consists of a component protocol, leaf objects, and composite objects.
  • This pattern is especially useful for hierarchical data structures like file systems or UI hierarchies.

Explain the Coordinator Pattern in iOS

· 5 min read
Szymon Michalak
iOS Developer
Sources & Resources

Main Source: Ray Wenderlich - Design Patterns by Tutorials (2019)
Further Reading:

TL/DR

The Coordinator Pattern centralizes navigation logic in iOS applications, organizing flow logic between view controllers by separating that responsibility into a dedicated coordinator object.

Concept Overview​

The Coordinator Pattern is a structural design pattern that manages the flow between view controllers, decoupling the navigation logic from individual view controllers. The key components of this pattern are:

  1. Coordinator Protocol: Defines the methods that all coordinators must implement.
  2. Concrete Coordinator: Implements the coordinator protocol and manages specific flows.
  3. Router Protocol: Manages the presentation and dismissal of view controllers.
  4. Concrete Router: Implements the router protocol, handling actual navigation actions.
  5. View Controllers: Delegates navigation logic to the coordinator, keeping their logic focused on UI concerns.

Playground Example​

Here is the complete example from the book that demonstrates the How to Code flow using the Coordinator Pattern:

import UIKit

// MARK: - Router Protocol
public protocol Router: class {
func present(_ viewController: UIViewController, animated: Bool)
func present(_ viewController: UIViewController, animated: Bool, onDismissed: (() -> Void)?)
func dismiss(animated: Bool)
}

extension Router {
public func present(_ viewController: UIViewController, animated: Bool) {
present(viewController, animated: animated, onDismissed: nil)
}
}

// Concrete Router
public class NavigationRouter: NSObject, Router, UINavigationControllerDelegate {
private let navigationController: UINavigationController

public init(navigationController: UINavigationController) {
self.navigationController = navigationController
super.init()
navigationController.delegate = self
}

public func present(_ viewController: UIViewController, animated: Bool, onDismissed: (() -> Void)?) {
navigationController.pushViewController(viewController, animated: animated)
}

public func dismiss(animated: Bool) {
navigationController.popViewController(animated: animated)
}
}

// MARK: - Coordinator Protocol
public protocol Coordinator {
var children: [Coordinator] { get set }
var router: Router { get }
func start()
}

// MARK: - HowToCodeCoordinator
public class HowToCodeCoordinator: Coordinator {
public var children = [Coordinator]()
public let router: Router
private lazy var stepViewControllers: [StepViewController] = {
return [
StepViewController.instantiate(delegate: self, buttonColor: .red, text: "First step", title: "Step 1"),
StepViewController.instantiate(delegate: self, buttonColor: .orange, text: "Second step", title: "Step 2"),
StepViewController.instantiate(delegate: self, buttonColor: .green, text: "Third step", title: "Step 3")
]
}()

public init(router: Router) {
self.router = router
}

public func start() {
let firstViewController = stepViewControllers.first!
router.present(firstViewController, animated: true, onDismissed: nil)
}

public func showNextStep(from viewController: StepViewController) {
guard let index = stepViewControllers.firstIndex(of: viewController),
index + 1 < stepViewControllers.count else {
return
}
let nextViewController = stepViewControllers[index + 1]
router.present(nextViewController, animated: true, onDismissed: nil)
}
}

// MARK: - StepViewController
public class StepViewController: UIViewController {
var delegate: StepViewControllerDelegate?
var buttonColor: UIColor?
var stepText: String?
var stepTitle: String?

static func instantiate(delegate: StepViewControllerDelegate, buttonColor: UIColor, text: String, title: String) -> StepViewController {
let viewController = StepViewController()
viewController.delegate = delegate
viewController.buttonColor = buttonColor
viewController.stepText = text
viewController.stepTitle = title
return viewController
}

override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = buttonColor
let label = UILabel()
label.text = stepText
label.textAlignment = .center
label.frame = CGRect(x: 0, y: 0, width: 300, height: 50)
label.center = view.center
view.addSubview(label)

let button = UIButton(type: .system)
button.setTitle("Next Step", for: .normal)
button.addTarget(self, action: #selector(nextTapped), for: .touchUpInside)
button.frame = CGRect(x: 0, y: 100, width: 200, height: 50)
view.addSubview(button)
}

@objc func nextTapped() {
delegate?.stepViewControllerDidPressNext(self)
}
}

// MARK: - StepViewControllerDelegate Protocol
public protocol StepViewControllerDelegate {
func stepViewControllerDidPressNext(_ controller: StepViewController)
}

extension HowToCodeCoordinator: StepViewControllerDelegate {
public func stepViewControllerDidPressNext(_ controller: StepViewController) {
showNextStep(from: controller)
}
}

// Example usage
let navigationController = UINavigationController()
let router = NavigationRouter(navigationController: navigationController)
let coordinator = HowToCodeCoordinator(router: router)
coordinator.start()

PlaygroundPage.current.liveView = navigationController

How It Works:​

  • Coordinator: The HowToCodeCoordinator manages the sequence of view controllers and handles navigation between steps.
  • Router: The NavigationRouter is responsible for presenting and dismissing the view controllers.
  • View Controllers: Each StepViewController displays content and delegates the "next" action back to the coordinator.

When to Use​

  • Managing complex navigation: If your app has multiple view controllers with complex navigation flows.
  • Decoupling navigation: When you want to keep navigation logic separate from view controllers to improve modularity and testability.

When to Be Careful​

  1. Overuse in Simple Apps: If your app is simple with minimal navigation, using the Coordinator Pattern may introduce unnecessary complexity. In these cases, sticking to view controller-based navigation can be more efficient.

  2. Memory Management: Coordinators that manage a large number of children or view controllers must be careful with memory management, particularly in terms of retaining view controllers or child coordinators for too long. Use weak references to avoid memory leaks.

  3. Handling Deep Links: When implementing deep linking, the coordinator must be able to navigate to specific screens based on external inputs. This can make the flow logic more complex, requiring additional work to manage and handle multiple potential entry points into the flow.

  4. Maintaining State Across Coordinators: Managing application state when using multiple coordinators can become difficult. You need to decide how to share state between coordinators and ensure that state is managed appropriately across flows without adding too much interdependence between them.

  5. Testing: Although the Coordinator Pattern makes the navigation logic more modular, testing flows becomes more complex as the number of coordinators grows. Coordinators often need to be mocked or stubbed in order to fully test the logic.


In Bullets
  • The Coordinator Pattern centralizes navigation logic and improves modularity.
  • Involves coordinators, routers, and view controllers.
  • Useful for managing complex navigation flows.
  • Be cautious of overusing it in small apps and handling memory and deep linking carefully.

Explain the Delegation Pattern in iOS

· 3 min read
Szymon Michalak
iOS Developer
Sources & Resources

Main Source: Ray Wenderlich - Design Patterns by Tutorials (2019)

Further Reading:

TL/DR

The Delegation Pattern is a design pattern that enables one object (the delegator) to assign tasks to another object (the delegate). The delegate object must conform to a specific protocol, which defines the methods it must implement.

Concept Overview​

The Delegation Pattern is fundamental in iOS, often used to enable one object to delegate tasks to another. This decouples the logic and allows each component to focus on a specific responsibility. In this pattern:

  • Delegator: The object that assigns tasks to the delegate.
  • Delegate: The object that performs the tasks, adhering to the delegate protocol.

This pattern is prevalent in UIKit components like UITableView and UITextField.

How Delegation Works:​

  1. Delegator Object: The delegator keeps a reference to the delegate and assigns tasks.
  2. Protocol Definition: The protocol defines the methods the delegate must implement.
  3. Delegate Object: The delegate adheres to the protocol and performs the assigned tasks.

Key Benefits:​

  • Separation of Concerns: The pattern allows breaking large classes into smaller, focused ones.
  • Reusability: The same delegator can work with multiple delegates that conform to the protocol, making it flexible.

Playground Example​

Here’s a full example of using the Delegation Pattern with a UITableView and custom protocol.

import UIKit

// Define a protocol
protocol MenuViewControllerDelegate: AnyObject {
func menuViewController(_ menuViewController: MenuViewController, didSelectItemAtIndex index: Int)
}

// Create the delegator
public class MenuViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

@IBOutlet public var tableView: UITableView! {
didSet {
tableView.dataSource = self
tableView.delegate = self
}
}

private let items = ["Item 1", "Item 2", "Item 3"]
public weak var delegate: MenuViewControllerDelegate?

public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}

public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = items[indexPath.row]
return cell
}

public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
delegate?.menuViewController(self, didSelectItemAtIndex: indexPath.row)
}
}

How It Works:​

  • Delegator: The MenuViewController assigns tasks to the delegate when a table row is selected.
  • Delegate: The object implementing the MenuViewControllerDelegate protocol reacts when an item is selected.

When to Use​

  • Breaking Down Large Classes: Use delegation when you need to split large classes into smaller, more manageable ones.
  • Flexible Communication: Delegation is useful when different objects need to communicate, allowing flexibility through protocols.

When to Be Careful​

  • Retain Cycles: Delegates are often declared as weak references to prevent retain cycles. Be cautious of strong references, which can cause memory leaks.
  • Overuse: While helpful, having too many delegates in one object can make the class difficult to manage. If a class relies on multiple delegates, it may be doing too much and needs to be refactored.

In Bullets
  • Delegation allows one object to delegate tasks to another.
  • Delegate Protocol: Defines the methods the delegate must implement.
  • Weak References: Commonly used to avoid retain cycles in memory management.

Explain the Facade Pattern in iOS

· 3 min read
Szymon Michalak
iOS Developer
Sources & Resources

Main Source: Ray Wenderlich - Design Patterns by Tutorials (2019)
Further Reading:

TL/DR

The Facade Pattern provides a simplified interface to a complex system by hiding the complexity of subsystems, allowing consumers to interact with the system through a unified interface.

Concept Overview​

The Facade Pattern is a structural design pattern that simplifies interaction with complex systems by providing a single interface. It consists of:

  1. Facade: A simplified interface that interacts with multiple subsystem classes.
  2. Subsystem Classes: The actual implementation classes that perform specific tasks.

This pattern is especially useful when a system contains several interconnected components, and you want to expose a single entry point for users to perform tasks.

Playground Example​

The following example shows how the Facade Pattern works in an ordering system:

import Foundation

// MARK: - Dependencies
public struct Customer {
public let identifier: String
public var address: String
public var name: String
}

public struct Product {
public let identifier: String
public var name: String
public var cost: Double
}

// Inventory Database
public class InventoryDatabase {
public var inventory: [Product: Int] = [:]

public init(inventory: [Product: Int]) {
self.inventory = inventory
}
}

// Shipping Database
public class ShippingDatabase {
public var pendingShipments: [Customer: [Product]] = [:]
}

// MARK: - Facade
public class OrderFacade {
public let inventoryDatabase: InventoryDatabase
public let shippingDatabase: ShippingDatabase

public init(inventoryDatabase: InventoryDatabase, shippingDatabase: ShippingDatabase) {
self.inventoryDatabase = inventoryDatabase
self.shippingDatabase = shippingDatabase
}

public func placeOrder(for product: Product, by customer: Customer) {
print("Place order for '\(product.name)' by '\(customer.name)'")

let count = inventoryDatabase.inventory[product, default: 0]
guard count > 0 else {
print("'\(product.name)' is out of stock!")
return
}

inventoryDatabase.inventory[product] = count - 1

var shipments = shippingDatabase.pendingShipments[customer, default: []]
shipments.append(product)
shippingDatabase.pendingShipments[customer] = shipments

print("Order placed for '\(product.name)' by '\(customer.name)'")
}
}

// Example usage
let product = Product(identifier: "product-001", name: "Ray's Doodle", cost: 19.99)
let customer = Customer(identifier: "customer-001", address: "123 Apple St.", name: "Johnny Appleseed")
let inventoryDatabase = InventoryDatabase(inventory: [product: 50])
let shippingDatabase = ShippingDatabase()

let orderFacade = OrderFacade(inventoryDatabase: inventoryDatabase, shippingDatabase: shippingDatabase)
orderFacade.placeOrder(for: product, by: customer)

How It Works:​

  • Facade: OrderFacade simplifies the interaction by exposing a method placeOrder, which internally interacts with InventoryDatabase and ShippingDatabase.
  • Subsystem Classes: These handle specific responsibilities such as managing inventory and handling pending shipments.

When to Use​

  • Simplify Complex Systems: When you want to make complex systems easier to use.
  • Decouple Subsystems: When you need to hide the complexities of the subsystems from the client.

When to Be Careful​

  • God Facade: Avoid making the facade too complex. It should simplify the system without adding additional complexity or responsibility.

In Bullets
  • The Facade Pattern provides a simplified interface to a complex system.
  • It consists of a facade and subsystem classes.
  • Useful for simplifying interactions with complex components.

Explain the Factory Pattern in iOS

· 3 min read
Szymon Michalak
iOS Developer
Sources & Resources

Main Source: Ray Wenderlich - Design Patterns by Tutorials (2019)
Further Reading:

TL/DR

The Factory Pattern is a creational pattern that provides a way to create objects without exposing the instantiation logic to the client. It delegates the responsibility of creating instances to a separate factory class.

Concept Overview​

The Factory Pattern consists of two main components:

  1. Factory: Handles the logic of object creation.
  2. Product: The object that is created by the factory.

This pattern is particularly useful in iOS for scenarios where you need to create objects with multiple configurations or polymorphic objects that implement a common interface or inherit from a base class.

How Factory Works:​

  1. Factory: The factory class contains the method to instantiate various types of products.
  2. Product: The objects returned by the factory, which could be any subtype of a common interface or superclass.

Key Benefits:​

  • Decouples Object Creation: Keeps object creation logic separate from the client.
  • Encapsulation: Simplifies client code by hiding instantiation details.

Playground Example​

Here is an example of using the Factory Pattern to create email responses for job applicants:

import Foundation

// MARK: - Models
public struct JobApplicant {
public let name: String
public let email: String
public var status: Status

public enum Status {
case new, interview, hired, rejected
}
}

public struct Email {
public let subject: String
public let messageBody: String
public let recipientEmail: String
public let senderEmail: String
}

// MARK: - Factory
public struct EmailFactory {
public let senderEmail: String

public func createEmail(to recipient: JobApplicant) -> Email {
let subject: String
let messageBody: String

switch recipient.status {
case .new:
subject = "We Received Your Application"
messageBody = "Thanks for applying, \(recipient.name)!"
case .interview:
subject = "Interview Invitation"
messageBody = "Dear \(recipient.name), please come for an interview."
case .hired:
subject = "Congratulations!"
messageBody = "Welcome to the team, \(recipient.name)!"
case .rejected:
subject = "Thank You for Applying"
messageBody = "Dear \(recipient.name), we regret to inform you that we have moved forward with other candidates."
}

return Email(subject: subject, messageBody: messageBody, recipientEmail: recipient.email, senderEmail: senderEmail)
}
}

// Usage Example
let applicant = JobApplicant(name: "John Doe", email: "john@example.com", status: .interview)
let emailFactory = EmailFactory(senderEmail: "hr@example.com")
let email = emailFactory.createEmail(to: applicant)

print(email.subject) // Interview Invitation

How It Works:​

  • Factory: The EmailFactory class is responsible for creating different types of emails based on the applicant’s status.
  • Product: The Email object is the product created by the factory.

When to Use​

  • Polymorphic Object Creation: When you have a group of related objects and need to manage their creation dynamically.
  • Simplifying Code: To reduce duplication by centralizing object creation in a single factory.

When to Be Careful​

  • Overuse: Simple object creation doesn’t always require a factory, so only use this pattern when needed.

In Bullets
  • The Factory Pattern simplifies object creation by delegating it to a factory.
  • It involves a factory and products that share a common interface or superclass.
  • Useful for managing complex creation logic, especially for polymorphic objects.