Friday, 6 October 2023

MVC, MVVM, and MVP design patterns in Swift

 Hello everyone! I’m excited to share my knowledge about architecture design patterns which are (MVC, MVVM, and MVP) and how it works in Swift. 

# MVC stands for Model-View-Controller

The MVC architecture is straightforward to understand and quite easy to implement in UIKit. All architectures are created to make it easy, effective, flexible, and fast to develop our app and prevent many mistakes that developers can make without architecture.

Let’s dive into the MVC architecture and gain a deeper understanding of how it works 🔥

# M — Model
The “Model” is responsible for business logic, network calls, parse data, and so on. The “Model” does not need to know about the UI if you in the “Model” use UI attributes it means your architecture goes wrong.

# V — View
The ‘View’ is responsible for representing UI elements such as UILabel, UIView, UITextView, and so on. It should not have knowledge of the ‘Model.’ Including business logic and network calls within the ‘View’ indicates a problem in your architecture.

# C — Controller
The “Controller” acts as an intermediary between the “Model” and the “View”. It handles user input, communicates with the “Model” to fetch or update data, and updates the “View” accordingly. You can see the above image 🤷‍♂️

# The real example of MVC architecture in UIKit 🥶

You can check out my GitHub repository at the following link: 👉 TodoListApp. The ‘TodoListApp’ is built using the MVC architecture.

For the “Model” check out this page.
For the “View” check out this page.
For the “Controller” check out this page.

# MVVM stands for Model-View-ViewModel

The MVVM architecture is quite similar to the MVC design pattern. While it shares many similarities with MVC, MVVM is widely used in SwiftUI, and it’s especially beneficial when working with the Combine framework.

# M — Model
The “Model” in MVVM is similar to the “Model” in MVC. It represents the data and business logic of the application. It defines how data is structured and manipulated. In Swift, the “Model” can be implemented using classes, structs, or enums.

# V — View
The “View” in MVVM is also similar to the “View” in MVC. It represents the only UI elements and the “View” should only display data, and handle user interactions, without containing application logic(business logic).

# VM — View-Model
The “ViewModel” acts as a mediator between the “Model” and the “View”. The “ViewModel” is responsible for preparing and formatting data from the “Model” for presentation in the “View”. “ViewModels” also handles user input and interactions. They contain the business logic needed for specific views.

# The real example of MVVM architecture in UIKit 🥶

# Model

struct Task {
var title: String
var completed: Bool
}

# ViewModel

class TaskListViewModel {
private var tasks: [Task] = [
Task(title: "Buy groceries", completed: false),
Task(title: "Read a book", completed: true),
Task(title: "Exercise", completed: false)
]

var taskCount: Int {
return tasks.count
}

func task(at index: Int) -> Task {
return tasks[index]
}

func addTask(title: String) {
let newTask = Task(title: title, completed: false)
tasks.append(newTask)
}

func toggleTask(at index: Int) {
tasks[index].completed.toggle()
}
}

# View

import UIKit

class TaskListViewController: UIViewController {
@IBOutlet private var tableView: UITableView!
@IBOutlet private var taskTextField: UITextField!

private var viewModel = TaskListViewModel()

override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
}

@IBAction private func addButtonTapped() {
guard let taskTitle = taskTextField.text, !taskTitle.isEmpty else { return }
viewModel.addTask(title: taskTitle)
taskTextField.text = ""
tableView.reloadData()
}
}

extension TaskListViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.taskCount
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TaskCell", for: indexPath)
let task = viewModel.task(at: indexPath.row)
cell.textLabel?.text = task.title
cell.accessoryType = task.completed ? .checkmark : .none
return cell
}
}
  1. Task represents the “Model”, which defines the structure of a task.
  2. TaskListViewModel is the “ViewModel”, responsible for managing tasks, adding tasks, and updating their completion status.
  3. TaskListViewController is the “View”, which displays the list of tasks in a table view and allows the user to add new tasks. It communicates with the ViewModel to perform these tasks.

# MVP stands for Model-View-Presenter

The MVP architecture is also similar to MVC architecture the same “Model” layer, “View” layer, and additional “Presenter”. However, the MVP design pattern has one difference from the MVC design pattern. Now the ViewController(Controller layer) is considered as a view. which means it will include only the view-related code, nothing more and all logic will be implemented in the presenter.

  • View: The “View” includes both view and view controllers, taking care of setting up the user interface and handling user interactions and all events.
  • Presenter: The “Presenter” handles all the logic, such as responding to user actions and updating the UI through the Protocols and Delegates. An important aspect is that our presenter is UIKit-independent, making it well-isolated and easily testable.
  • Model: The “Model” role will be exactly the same in MVC architecture.

# The real example of MVP architecture in UIKit 🥶

# Model

// Model: Represents a task
struct Task {
var title: String
var description: String
}

# View

// View: Displays task information
class TaskViewController: UIViewController {
@IBOutlet private var titleLabel: UILabel!
@IBOutlet private var descriptionLabel: UILabel!

private var presenter: TaskPresenterProtocol!

convenience init(presenter: TaskPresenterProtocol) {
self.init()
self.presenter = presenter
}

override func viewDidLoad() {
super.viewDidLoad()
configureView()
}

private func configureView() {
let task = presenter.fetchTask()
titleLabel.text = task.title
descriptionLabel.text = task.description
}

@IBAction private func completeButtonTapped() {
presenter.taskCompleted()
}
}

# Presenter

// Presenter: Manages the interaction between Model and View
protocol TaskPresenterProtocol {
func fetchTask() -> Task
func taskCompleted()
}

class TaskPresenter: TaskPresenterProtocol {
private var task: Task

init(task: Task) {
self.task = task
}

func fetchTask() -> Task {
return task
}

func taskCompleted() {
// Perform logic to mark the task as completed
}
}
  • Task: The task represents the “Model”, which defines the structure of a task.
  • TaskPresenter: The TaskPresenter is the “Presenter”, which manages the interaction between the “Model” and the “View”. It provides methods to fetch task data and mark tasks as completed.
  • TaskViewController: The TaskViewController is the “View”, which displays the task information and handles user interactions. It receives a TaskPresenterProtocol instance and uses it to fetch and update task data.

GCD — Grand Central Dispatch in iOS

 

Grand Central Dispatch 🚦

Hello everyone! I’m excited to share my knowledge about GCD — Grand Central Dispatch in iOS and how it works in Swift.

# Problem ðŸ˜•

Very often we are working with large items that can freeze our apps or hang which is not user-friendly. When we work with the backend and transform or get large data we can do heavy work at the background thread and do not overload the Main Thread.

# Solution ðŸ˜Š

The GCD — Grand Central Dispatch gives us many opportunities to control our data flow and manage our threads which is the most important.

Let’s go through the GCD and fully understand what is it.

How does Grand Central Dispatch work 🚦

Grand Central Dispatch, or GCD, is a low-level API designed for managing concurrent operations. It enhances your application’s smoothness, responsiveness, and overall performance. Oftentimes, developers encounter issues such as application hang or freezing when attempting to perform multiple tasks simultaneously. This is a common challenge, and that’s where GCD comes into play, allowing you to efficiently manage multiple tasks concurrently.

I am sure when you work with the network and get an image from the network and pass that image to your UIImageView.image it crashes your app or gives you information that you are not in the Main thread and cannot assign the image to UIImageView.image.

Very often you use this structure code to solve that problem.

DispatchQueue.main.async {
// preform your async code here
// for example
// UIImageView.image = myImageFromNetwork
}

All UIKit elements, such as UIImageView, UILabel, UITextView, and more, are positioned on the Main Thread. When working with network requests, they are not on the Main Thread, necessary the use the code mentioned above to assign images.

The most important thing to know is that GCD manages the tasks FIFO order.

# Concurrent

What does ‘concurrent’ mean? The GCD allows tasks to be executed concurrently on multiple CPU cores, which means it executes multiple tasks at the same time, but it’s not guaranteed that they will finish at the same time. Additionally, they can finish in any order.

Concurrent Queue

# Serial

What does ‘serial’ mean? The GCD typically executes tasks on a single thread of execution. This means that only one task runs at any given moment, and tasks are processed sequentially.

Serial Queue

Queues can be either “serial” or “concurrent”.
Serial Queues guarantee that only one task runs at any given time.
Concurrent queues allow multiple tasks to run at the same time. The queue guarantees tasks start in the order you add them. Tasks can finish in any order.

# Understanding Queue Types 🤫

The GCD provides three main types of queues:
Main queue: Runs on the main thread and is a serial queue.

Global queues: Concurrent queues shared by the whole system. Four such queues exist, each with different priorities: “high”, “default”, “low”, and “background”. The background priority queue has the lowest priority and is throttled in any I/O activity to minimize negative system impact.

Custom queues: Queues you create that can be “serial” or “concurrent”.

# Custom Queues in action 🔥

# Serial queue

// Creating a custom serial queue with a specific label
let customQueue = DispatchQueue(label: "com.example.myqueue")

// Adding a task to the custom queue
customQueue.async {
// Perform some work here
}

# Concurrent queue

// Creating a custom concurrent queue with a specific label
let customConcurrentQueue = DispatchQueue(label: "com.example.myconcurrentqueue", attributes: .concurrent)

// Adding tasks to the custom concurrent queue
customConcurrentQueue.async {
// Task 1: Perform some work here
}

customConcurrentQueue.async {
// Task 2: Perform some work here
}

customConcurrentQueue.async {
// Task 3: Perform some work here
}

# About Quality of Service Classes(QoS)

  • User-interactive: This represents tasks that must complete immediately to provide a nice user experience. Use it for UI updates, event handling and small workloads that require low latency. The total amount of work done in this class during the execution of your app should be small. This should run on the main thread.
  • User-initiated: The user initiates these asynchronous tasks from the UI. Use them when the user is waiting for immediate results and for tasks required to continue user interaction. They execute in the high-priority global queue.
  • Utility: This represents long-running tasks, typically with a user-visible progress indicator. Use it for computations, I/O, networking, continuous data feeds and similar tasks. This class is designed to be energy efficient. This gets mapped into the low-priority global queue.
  • Background: This represents tasks the user isn’t directly aware of. Use it for prefetching, maintenance and other tasks that don’t require user interaction and aren’t time-sensitive. This gets mapped into the background priority global queue.
  • Default: The priority level of this QoS falls between user-initiated and utility. This QoS is not intended to be used by developers to classify work. Work that has no QoS information assigned is treated as default, and the GCD global queue runs at this level.
  • Unspecified: This represents the absence of QoS information and cues the system that an environmental QoS should be inferred. Threads can have an unspecified QoS if they use legacy APIs that may opt the thread out of QoS.

Let’s take a look at a few examples.

# Perform background tasks and also update UI.

DispatchQueue.global(qos: .background).async { 
// Call your background task
DispatchQueue.main.async {
// UI Updates here for task complete.
}
}

# DispatchQueue with delay ⏳

let deadlineTime = DispatchTime.now() + .seconds(1)
DispatchQueue.main.asyncAfter(deadline: deadlineTime) {
//Perform code here
}

# Sync and Async

func syncExamples() {
let people = DispatchQueue(label: "syncExamples.queue.people")
let age = DispatchQueue(label: "syncExamples.queue.age")

people.sync {
for name in ["Jack", "John", "Macheal"] {
print(name)
}
}

age.sync {
for age in [29, 34, 73] {
print(age)
}
}
}

syncExamples()
func asyncExamples() {
let people = DispatchQueue(label: "syncExamples.queue.people")
let age = DispatchQueue(label: "syncExamples.queue.age")

people.async {
for name in ["Jack", "John", "Macheal"] {
print(name)
}
}

age.async {
for age in [29, 34, 73] {
print(age)
}
}
}

asyncExamples()

If you want to fully understand, you should create your own example and add more print statements to track what happens.