Swift Design Patterns vs Architectural Patterns

Mastering Code Structure and Organization in Swift

Vikram Kumar
7 min readJun 21, 2024

Swift, Apple’s powerful and intuitive programming language, offers developers numerous ways to structure and organize their code. Two critical concepts in this regard are design patterns and architectural patterns. Both serve distinct purposes in software development and are essential for creating scalable, maintainable, and efficient applications. This article explores the differences between these two concepts, provides examples, and discusses how to effectively use them in Swift development.

Design patterns solve specific problems at the component level with reusable solutions, while architectural patterns provide high-level frameworks for organizing the overall structure and design of an entire application.

Understanding Design Patterns

Design patterns are general, reusable solutions to common problems in software design. They provide a template that can be applied in various situations to solve specific problems. Design patterns are not finished designs that can be directly transformed into code; rather, they are templates that developers can customize to solve particular issues.

Photo by Joshua Aragon on Unsplash

Types of Design Patterns

Design patterns are typically classified into three categories: creational, structural, and behavioral.

  1. Creational Patterns: These patterns deal with object creation mechanisms, trying to create objects in a manner suitable to the situation. The primary goal is to reduce the complexities and instabilities in object creation.
  • Singleton: Ensures a class has only one instance and provides a global point of access to it.
  • Factory: Defines an interface for creating objects, but lets subclasses alter the type of objects that will be created.
  • Builder: Separates the construction of a complex object from its representation.

2. Structural Patterns: These patterns deal with object composition or the way objects are structured to form larger structures.

  • Adapter: Allows incompatible interfaces to work together.
  • Composite: Composes objects into tree structures to represent part-whole hierarchies.
  • Decorator: Adds behavior to objects dynamically.

3. Behavioral Patterns: These patterns are concerned with algorithms and the assignment of responsibilities between objects.

  • Observer: Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified.
  • Strategy: Defines a family of algorithms, encapsulates each one, and makes them interchangeable.
  • Command: Encapsulates a request as an object, thereby allowing for parameterization of clients with queues, requests, and operations.

Design Pattern Examples

Singleton Pattern

The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. This is useful for managing shared resources such as database connections or configuration settings.

class DatabaseManager {
// The single instance of DatabaseManager
static let shared = DatabaseManager()

// Private initializer to prevent the creation of additional instances
private init() {
// Initialize the database connection
}

// A method to fetch data from the database
func fetchData() -> [String] {
// Implement the data fetching logic
return ["Data1", "Data2", "Data3"]
}
}

// Usage
let databaseManager = DatabaseManager.shared
let data = databaseManager.fetchData()
print(data)

Observer Pattern

The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified. This pattern is often used in SwiftUI and Combine.

import Combine

class DataModel: ObservableObject {
// Published property that observers can subscribe to
@Published var data: String = "Initial Data"
}

class DataObserver {
var cancellable: AnyCancellable?

init(dataModel: DataModel) {
// Subscribe to the data model's data property
cancellable = dataModel.$data.sink { newData in
print("Data has changed to: \(newData)")
}
}
}

// Usage
let dataModel = DataModel()
let dataObserver = DataObserver(dataModel: dataModel)

// Update the data property
dataModel.data = "Updated Data"

Adapter Pattern

The Adapter pattern allows incompatible interfaces to work together. It is often used to integrate new functionality into an existing system without modifying the existing code.

// Existing class with an incompatible interface
class OldPrinter {
func printOldStyle(message: String) {
print("Printing in old style: \(message)")
}
}

// New interface that we want to adapt to
protocol NewPrinterProtocol {
func printNewStyle(message: String)
}

// Adapter class that implements the new interface
class PrinterAdapter: NewPrinterProtocol {
private let oldPrinter: OldPrinter

init(oldPrinter: OldPrinter) {
self.oldPrinter = oldPrinter
}

func printNewStyle(message: String) {
// Adapt the old method to the new interface
oldPrinter.printOldStyle(message: message)
}
}

// Usage
let oldPrinter = OldPrinter()
let printerAdapter = PrinterAdapter(oldPrinter: oldPrinter)
printerAdapter.printNewStyle(message: "Hello, World!")

Understanding Architectural Patterns

Architectural patterns, on the other hand, are high-level strategies that concern the overall structure and organization of a software system. They are more abstract than design patterns and deal with the system as a whole.

Types of Architectural Patterns

Common architectural patterns include MVC (Model-View-Controller), MVVM (Model-View-ViewModel), and VIPER (View-Interactor-Presenter-Entity-Router).

  1. MVC (Model-View-Controller): This pattern divides the application into three interconnected components.
  • Model: Manages the data and business logic.
  • View: Displays the data to the user and sends user commands to the controller.
  • Controller: Interprets user inputs and requests from the view, manipulates the model, and updates the view.

2. MVVM (Model-View-ViewModel): This pattern is an evolution of MVC, which introduces a new component, the ViewModel, to handle the presentation logic.

  • Model: Same as MVC.
  • View: Same as MVC.
  • ViewModel: Converts data from the model into a format that can be easily displayed in the view.

3. VIPER (View-Interactor-Presenter-Entity-Router): A more complex pattern aimed at better separation of concerns and scalability.

  • View: Displays the data and relays user inputs.
  • Interactor: Contains the business logic.
  • Presenter: Prepares data for display and updates the view.
  • Entity: Represents the data.
  • Router: Handles navigation.

Detailed Architectural Pattern Examples

MVC (Model-View-Controller)

The MVC pattern divides the application into three interconnected components: Model, View, and Controller. This pattern is commonly used in UIKit-based applications.

import UIKit

// Model
struct User {
var name: String
var age: Int
}

// View
class UserView: UIView {
var nameLabel: UILabel!
var ageLabel: UILabel!

override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}

required init?(coder: NSCoder) {
super.init(coder: coder)
setupView()
}

private func setupView() {
nameLabel = UILabel()
ageLabel = UILabel()
// Configure and add labels to the view
}

func update(with user: User) {
nameLabel.text = user.name
ageLabel.text = "\(user.age) years old"
}
}

// Controller
class UserController: UIViewController {
var userView: UserView!
var user: User! {
didSet {
userView.update(with: user)
}
}

override func loadView() {
userView = UserView()
view = userView
}

override func viewDidLoad() {
super.viewDidLoad()
// Initialize user data
user = User(name: "John Doe", age: 30)
}
}

// Usage
let userController = UserController()
userController.loadViewIfNeeded()
print(userController.userView.nameLabel.text) // Output: "John Doe"

MVVM (Model-View-ViewModel)

The MVVM pattern introduces a ViewModel component to handle the presentation logic. This pattern is particularly useful in SwiftUI applications.

import SwiftUI

// Model
struct User: Identifiable {
var id: UUID
var name: String
var age: Int
}

// ViewModel
class UserViewModel: ObservableObject {
@Published var user: User

init(user: User) {
self.user = user
}

// A method to update the user's age
func updateAge(to newAge: Int) {
user.age = newAge
}
}

// View
struct UserView: View {
@ObservedObject var viewModel: UserViewModel

var body: some View {
VStack {
Text(viewModel.user.name)
Text("\(viewModel.user.age) years old")
Button(action: {
viewModel.updateAge(to: viewModel.user.age + 1)
}) {
Text("Increase Age")
}
}
}
}

// Usage
let user = User(id: UUID(), name: "Jane Doe", age: 25)
let viewModel = UserViewModel(user: user)
let userView = UserView(viewModel: viewModel)

// Display the SwiftUI view in a SwiftUI environment
struct ContentView: View {
var body: some View {
userView
}
}

VIPER (View-Interactor-Presenter-Entity-Router)

The VIPER pattern is a more complex architectural pattern aimed at better separation of concerns and scalability.

// Entity
struct User {
var name: String
var age: Int
}

// Interactor
protocol UserInteractorProtocol {
func fetchUser() -> User
}

class UserInteractor: UserInteractorProtocol {
func fetchUser() -> User {
return User(name: "Alice", age: 28)
}
}

// Presenter
protocol UserPresenterProtocol {
func presentUser()
}

class UserPresenter: UserPresenterProtocol {
private let interactor: UserInteractorProtocol
private weak var view: UserViewProtocol?

init(interactor: UserInteractorProtocol, view: UserViewProtocol) {
self.interactor = interactor
self.view = view
}

func presentUser() {
let user = interactor.fetchUser()
view?.displayUser(name: user.name, age: user.age)
}
}

// View
protocol UserViewProtocol: AnyObject {
func displayUser(name: String, age: Int)
}

class UserViewController: UIViewController, UserViewProtocol {
private let presenter: UserPresenterProtocol
private var nameLabel: UILabel!
private var ageLabel: UILabel!

init(presenter: UserPresenterProtocol) {
self.presenter = presenter
super.init(nibName: nil, bundle: nil)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
super.viewDidLoad()
setupView()
presenter.presentUser()
}

private func setupView() {
nameLabel = UILabel()
ageLabel = UILabel()
// Configure and add labels to the view
}

func displayUser(name: String, age: Int) {
nameLabel.text = name
ageLabel.text = "\(age) years old"
}
}

// Router
class UserRouter {
static func assembleModule() -> UIViewController {
let view = UserViewController(presenter: UserPresenter(interactor: UserInteractor(), view: view))
return view
}
}

// Usage
let userViewController = UserRouter.assembleModule()

Key Differences Between Design Patterns and Architectural Patterns

  1. Scope:
  • Design patterns address specific problems within a particular context, focusing on a smaller scale (individual components or classes).
  • Architectural patterns address broader concerns, focusing on the overall structure and high-level design of the application.

2. Abstraction Level:

  • Design patterns are more concrete and can be directly applied to solve common design issues.
  • Architectural patterns are more abstract and provide a blueprint for structuring the entire system.

3. Purpose:

  • Design patterns improve code reusability, flexibility, and readability at the component level.
  • Architectural patterns ensure scalability, maintainability, and clear separation of concerns at the system level.

4. Implementation:

  • Design patterns can be implemented with minor changes and adaptations to fit the specific needs of the problem.
  • Architectural patterns require significant planning and consideration as they impact the whole system’s design and structure.

When to Use Which?

Design Patterns

  • When you need to solve a specific problem that recurs frequently in your codebase.
  • When you want to improve code readability and maintainability by using well-known solutions.
  • When you need to enhance the flexibility and reusability of individual components.

Architectural Patterns

  • When you are starting a new project and need to decide on a high-level structure for your application.
  • When your application needs to be scalable and maintainable in the long term.
  • When you want to enforce a clear separation of concerns and improve the overall organization of your codebase.

Conclusion

Both design patterns and architectural patterns play crucial roles in Swift development. While design patterns offer solutions to common problems at the component level, architectural patterns provide a blueprint for structuring and organizing the entire system. Understanding and effectively implementing these patterns can significantly enhance the quality, maintainability, and scalability of your Swift applications. By choosing the right pattern for the right problem, you can create robust, efficient, and well-organized codebases that stand the test of time.

--

--

Vikram Kumar
Vikram Kumar

Written by Vikram Kumar

I am Vikram, a Senior iOS Developer at Matellio Inc. focused on writing clean and efficient code. Complex problem-solver with an analytical and driven mindset.

Responses (2)