Feb 27, 2023

Feb 27, 2023

Feb 27, 2023

12 mins

12 mins

12 mins

Swift, Xcode

Swift, Xcode

Swift, Xcode

Mastering MVVM in SwiftUI

Mastering MVVM in SwiftUI

Mastering MVVM in SwiftUI

Welcome to our tutorial on the MVVM design pattern in SwiftUI! In this tutorial, we will explain the important concepts behind MVVM, including its components and how they work together. We will also give you advice and methods for using MVVM in your own projects.

After reading this tutorial, you will have a good understanding of MVVM and how it can assist you in writing simpler, more expandable code for your iOS apps.


Introduction to MVVM and its key concepts


MVVM (Model-View-ViewModel) is a design pattern used to organize the code for a user interface. It is an improved version of the MVC (Model-View-Controller) pattern and helps to better separate the presentation layer of an app from its underlying business logic. This makes it easier to manage the complexity of modern apps and allows for better code reuse and testability.

In summary, MVVM simplifies the process of writing clean, maintainable code for your iOS app.


The components of MVVM: Model, View, ViewModel



The Model represents the underlying data and business logic of the app. In the context of SwiftUI, it refers to the data that the app is working with, such as user and app data.

The View represents the user interface of the app. It displays the app's content and handles user interactions. In MVVM, the View is declarative and reactive, meaning that it automatically updates whenever the data in the ViewModel changes.

The ViewModel is like a middleman between the Model and View. It shows the data and actions that the View needs, and it manages the communication between the Model and View. The ViewModel doesn't know about the View, and it only works with the data and actions that the View requires to show.


Basic implementing MVVM in SwiftUI:


Let's first start with a basic example. This will help us better understand the concepts behind MVVM and how it can be applied to your own projects:



import SwiftUI
import Combine

/// Define the model for a Task
struct Task: Identifiable {
    let id = UUID()
    let description: String
    let isCompleted: Bool
}

/// Define a view model that exposes tasks from the model for the view to consume
class TaskViewModel: ObservableObject {
    /// The list of tasks
    @Published var tasks: [Task] = []
    
    /// Function to load tasks from data source
    func loadData() {
        let dailyTasks = [
            Task(description: "Connect OpenAi API", isCompleted: true),
            Task(description: "Weekly call with mobile team", isCompleted: false),
            Task(description: "Finish MVVM tutorial", isCompleted: false)
        ]
        tasks = dailyTasks
    }
    
    /// Function to save task to data source
    func saveTask(task: Task) {
        tasks.append(task)
    }
}

/// The TaskView is the main View of the app
struct TaskView: View {
    /// Create instance of TaskViewModel to interact with data
    @ObservedObject var viewModel = TaskViewModel()
 
    var body: some View {
        List(viewModel.tasks) { task in
            VStack(alignment:.leading){
                Text(task.description)
                    .font(.body)
                Text(task.isCompleted ? "Completed" : "To Do")
                    .font(.caption)
            }
        }.onAppear {
            /// Load data when view appears
            self.viewModel.loadData()
        }
 
        Button("Add New Task +", action: {
            /// Save new task when button is pressed
            viewModel.saveTask(task: Task(description: "New daily task",
                                           isCompleted: false))
        })
    }
}


Example defines a struct called Task that represents a task with a description and a Boolean flag indicating whether it is completed.

It also defines a TaskViewModel class that exposes an array of tasks and methods for loading and saving tasks.

Lastly, it defines a View called TaskView that displays a list of tasks and a button to add a new task.

The @ObservedObject property wrapper is used to enable the view to observe changes to the viewModel object.

The @Published property wrapper is used to make the tasks array observable so that changes to it can be published to the view. The onAppear modifier is used to load the tasks when the view appears.


Repository


One common way to implement MVVM is by using the Repository pattern to load data from a data source, such as a database or web service.

In this example, it is not necessary to add a Repository, but it could be a good idea for more complex apps.

The Repository pattern abstracts the data access layer of an application and decouples it from the rest of the app. By using a Repository, the ViewModel can interact with it to load and save data, instead of interacting directly with the data source.

Using a Repository can be especially helpful in a complex app with multiple data sources or complex data access requirements. Managing the data access layer directly in the ViewModel can be difficult in such cases, as the ViewModel may have to handle many different data sources and data access operations.

By using a Repository, you can move all of the data access logic out of the ViewModel, which can simplify the ViewModel and make it easier to manage.



import SwiftUI
import Combine
 
/// Define the model for a Task
struct Task: Identifiable {
    let id = UUID()
    let description: String
    let isCompleted: Bool
}
  
/// Define a view model that exposes tasks from the model for the view to consume
class TaskViewModel: ObservableObject {
    /// Create instance of TaskRepository to interact with data source
    private let taskRepository = TaskRepository()
 
    /// Published property wrapper allows other objects to subscribe to changes in tasks array
    @Published var tasks: [Task] = []
    private var cancellables: Set<AnyCancellable> = []
 
    init() {
        /// Subscribe to changes in tasks and assign them to tasks in TaskViewModel
        taskRepository.$tasks.assign(to: \.tasks, on: self)
            .store(in: &cancellables)
    }
 
    /// Function to load tasks from data source
    func loadData() {
        taskRepository.loadData()
    }
 
    /// Function to save task to data source
    func saveTask(task: Task) {
        taskRepository.saveTask(task: task)
    }
}
 
/// The Repository is responsible for loading the data from a data source
class TaskRepository {
    /// Published property wrapper allows other objects to subscribe to changes in tasks array
    @Published var tasks: [Task] = []
 
    /// Function to load tasks from data source
    func loadData() {
        let dailyTasks = [
            Task(description: "Connect OpenAi API", isCompleted: true),
            Task(description: "Weekly call with mobile team", isCompleted: false),
            Task(description: "Finish MVVM tutorial", isCompleted: false)
        ]
 
        tasks = dailyTasks
    }
 
    /// Function to save task to data source
    func saveTask(task: Task) {
        tasks.append(task)
    }
}

/// The TaskView is the main View of the app
struct TaskView: View {
    /// Create instance of TaskViewModel to interact with data
    @ObservedObject var viewModel = TaskViewModel()
 
    var body: some View {
        List(viewModel.tasks) { task in
            VStack(alignment:.leading){
                Text(task.description)
                    .font(.body)
                Text(task.isCompleted ? "Completed" : "To Do")
                    .font(.caption)
            }
        }.onAppear {
            /// Load data when view appears
            self.viewModel.loadData()
        }
 
        Button("Add New Task +", action: {
            /// Save new task when button is pressed
            viewModel.saveTask(task: Task(description: "New daily task",
                                           isCompleted: false))
        })
    }
}


In this example, the TaskRepository class is responsible for loading data from a data source, storing and managing task data, and providing methods for loading and saving tasks. The TaskViewModel exposes the data from the Task model in a way that is easy for the TaskView to consume.

Using a Repository like this is a good fit for the MVVM pattern because it separates data and logic in the app. All data access operations are handled by the Repository, instead of being scattered throughout the ViewModel.


Data Manager


Initializing the Repository in each ViewModel may not be the best approach. To avoid this, you can create a singleton DataManager class that initializes the repositories and provides a reference to them for the ViewModel.

Here is an example of how you can modify your code to use a DataManager singleton



import SwiftUI
import Combine

/// Define the model for a Task
struct Task: Identifiable {
    let id = UUID()
    let description: String
    let isCompleted: Bool
}

/// Define a view model that exposes tasks from the model for the view to consume
class TaskViewModel: ObservableObject {
    /// Use the task repository from the data manager to get tasks
    private let taskRepository = DataManager.shared.taskRepository
 
    /// Published property wrapper allows other objects to subscribe to changes in tasks array
    @Published var tasks: [Task] = []
    private var cancellables: Set<AnyCancellable> = []
 
    init() {
        /// Subscribe to changes in tasks and assign them to tasks in TaskViewModel
        taskRepository.$tasks.assign(to: \.tasks, on: self)
            .store(in: &cancellables)
    }
 
    /// Function to load tasks from data source
    func loadData() {
        taskRepository.loadData()
    }
 
    /// Function to save task to data source
    func saveTask(task: Task) {
        taskRepository.saveTask(task: task)
    }
}

/// The Repository is responsible for loading the data from a data source
class TaskRepository {
    /// Published property wrapper allows other objects to subscribe to changes in tasks array
    @Published var tasks: [Task] = []
 
    /// Function to load tasks from data source
    func loadData() {
        let dailyTasks = [
            Task(description: "Connect OpenAi API", isCompleted: true),
            Task(description: "Weekly call with mobile team", isCompleted: false),
            Task(description: "Finish MVVM tutorial", isCompleted: false)
        ]
 
        tasks = dailyTasks
    }
 
    /// Function to save task to data source
    func saveTask(task: Task) {
        tasks.append(task)
    }
}

/// Define a singleton data manager that provides a reference to the task repository
class DataManager {
    static let shared = DataManager()
    
    let taskRepository = TaskRepository()
}

/// The TaskView is the main View of the app
struct TaskView: View {
    /// Create instance of TaskViewModel to interact with data
    @ObservedObject var viewModel = TaskViewModel()
 
    var body: some View {
        List(viewModel.tasks) { task in
            VStack(alignment:.leading){
                Text(task.description)
                    .font(.body)
                Text(task.isCompleted ? "Completed" : "To Do")
                    .font(.caption)
            }
        }.onAppear {
            /// Load data when view appears
            self.viewModel.loadData()
        }
 
        Button("Add New Task +", action: {
            /// Save new task when button is pressed
            viewModel.saveTask(task: Task(description: "New daily task",
                                           isCompleted: false))
        })
    }
}


In this updated example, the DataManager is a singleton that initializes the TaskRepository and provides a reference to it for the TaskViewModel to use. This ensures that only a single instance of the TaskRepository is created, and it can be shared among multiple ViewModelobjects.


Managing multiple repositories with DataManager


Overall, using a DataManager as a singleton that initializes Repositories can provide several benefits. This approach ensures that there is only one instance of the DataManager, guarantees that all parts of the application are using the same Repositories, and makes it easier to manage dependencies between different parts of the application.

Here is an example of a DataManager class that can be used to manage the loading of data from multiple sources:


import SwiftUI
import Combine

/// Define the Task and User models
struct Task: Identifiable {
    let id = UUID()
    let description: String
    let isCompleted: Bool
}

struct User: Identifiable {
    let id = UUID()
    let name: String?
    let email: String?
}

/// Define a view model that exposes tasks from the model for the view to consume
class TaskViewModel: ObservableObject {
    /// Initialize repositories
    private let taskRepository = DataManager.shared.taskRepository
    private let userRepository = DataManager.shared.userRepository
    
    /// Published properties wrapper allows other objects to subscribe to changes
    @Published var tasks: [Task] = []
    @Published var user: User?
    private var cancellables: Set<AnyCancellable> = []
    
    init() {
        /// Subscribe to changes in tasks  and user data
        taskRepository.$tasks.assign(to: \.tasks, on: self)
            .store(in: &cancellables)
        userRepository.$user.assign(to: \.user, on: self)
            .store(in: &cancellables)
    }
    
    /// Function to load tasks from data source
    func loadData() {
        DataManager.shared.loadData()
    }
    
    /// Function to save task to data source
    func saveTask(task: Task) {
        taskRepository.saveTask(task: task)
    }
    
    /// Returns a greeting with the user's name (if available)
    func headerText() -> String {
        guard let name = user?.name else { return "Hi, here is your tasks:"}
        return "Hi, \(name) here is your tasks:"
    }
}

/// The Repository is responsible for loading the data from a data source
class TaskRepository {
    /// Published property wrapper allows other objects to subscribe to changes in tasks array
    @Published var tasks: [Task] = []
 
    /// Function to load tasks from data source
    func loadData() {
        let dailyTasks = [
            Task(description: "Connect OpenAi API", isCompleted: true),
            Task(description: "Weekly call with mobile team", isCompleted: false),
            Task(description: "Finish MVVM tutorial", isCompleted: false)
        ]
 
        tasks = dailyTasks
    }
 
    /// Function to save task to data source
    func saveTask(task: Task) {
        tasks.append(task)
    }
}

/// The UserRepository is responsible for loading user data
class UserRepository {
    @Published var user: User?
    
    /// Load the user data from the data source
    func loadData() {
        user = User(name: "Nazar", email: "nazar@tapforce.com")
    }
}

/// Define a singleton data manager that provides a reference to the task repository
class DataManager {
    static let shared = DataManager()
    
    let taskRepository = TaskRepository()
    let userRepository = UserRepository()
    
    /// Load data from the repositories
    func loadData() {
        taskRepository.loadData()
        userRepository.loadData()
    }
}

/// The TaskView is the main View of the app
struct TaskView: View {
    /// Create a TaskViewModel to manage data for the View
    @ObservedObject var viewModel = TaskViewModel()
    
    var body: some View {
        /// Present user welcome message
        Text(viewModel.headerText())
        
        /// Present tasks in a list with their descriptions and completion status
        List(viewModel.tasks) { task in
            VStack(alignment:.leading){
                Text(task.description)
                    .font(.body)
                Text(task.isCompleted ? "Completed" : "To Do")
                    .font(.caption)
            }
        }
        /// Load tasks when the view appears
        .onAppear {
            self.viewModel.loadData()
        }
        
        /// Allow the user to add a new task by clicking a button
        Button("Add New Task +", action: {
            viewModel.saveTask(task: Task(description: "New daily task",
                                          isCompleted: false))
        })
    }
}


This example demonstrates how DataManager can be used to manage multiple repositories and data sources:

When you add new data sources and repositories, you can simply add them to the DataManager and provide a reference to them for the ViewModel to use.

This approach is especially useful when you have multiple ViewModel objects that need to access the same data sources. By using the DataManager, you can avoid creating multiple instances of repositories and reduce the memory overhead of your application.

Welcome to our tutorial on the MVVM design pattern in SwiftUI! In this tutorial, we will explain the important concepts behind MVVM, including its components and how they work together. We will also give you advice and methods for using MVVM in your own projects.

After reading this tutorial, you will have a good understanding of MVVM and how it can assist you in writing simpler, more expandable code for your iOS apps.


Introduction to MVVM and its key concepts


MVVM (Model-View-ViewModel) is a design pattern used to organize the code for a user interface. It is an improved version of the MVC (Model-View-Controller) pattern and helps to better separate the presentation layer of an app from its underlying business logic. This makes it easier to manage the complexity of modern apps and allows for better code reuse and testability.

In summary, MVVM simplifies the process of writing clean, maintainable code for your iOS app.


The components of MVVM: Model, View, ViewModel



The Model represents the underlying data and business logic of the app. In the context of SwiftUI, it refers to the data that the app is working with, such as user and app data.

The View represents the user interface of the app. It displays the app's content and handles user interactions. In MVVM, the View is declarative and reactive, meaning that it automatically updates whenever the data in the ViewModel changes.

The ViewModel is like a middleman between the Model and View. It shows the data and actions that the View needs, and it manages the communication between the Model and View. The ViewModel doesn't know about the View, and it only works with the data and actions that the View requires to show.


Basic implementing MVVM in SwiftUI:


Let's first start with a basic example. This will help us better understand the concepts behind MVVM and how it can be applied to your own projects:



import SwiftUI
import Combine

/// Define the model for a Task
struct Task: Identifiable {
    let id = UUID()
    let description: String
    let isCompleted: Bool
}

/// Define a view model that exposes tasks from the model for the view to consume
class TaskViewModel: ObservableObject {
    /// The list of tasks
    @Published var tasks: [Task] = []
    
    /// Function to load tasks from data source
    func loadData() {
        let dailyTasks = [
            Task(description: "Connect OpenAi API", isCompleted: true),
            Task(description: "Weekly call with mobile team", isCompleted: false),
            Task(description: "Finish MVVM tutorial", isCompleted: false)
        ]
        tasks = dailyTasks
    }
    
    /// Function to save task to data source
    func saveTask(task: Task) {
        tasks.append(task)
    }
}

/// The TaskView is the main View of the app
struct TaskView: View {
    /// Create instance of TaskViewModel to interact with data
    @ObservedObject var viewModel = TaskViewModel()
 
    var body: some View {
        List(viewModel.tasks) { task in
            VStack(alignment:.leading){
                Text(task.description)
                    .font(.body)
                Text(task.isCompleted ? "Completed" : "To Do")
                    .font(.caption)
            }
        }.onAppear {
            /// Load data when view appears
            self.viewModel.loadData()
        }
 
        Button("Add New Task +", action: {
            /// Save new task when button is pressed
            viewModel.saveTask(task: Task(description: "New daily task",
                                           isCompleted: false))
        })
    }
}


Example defines a struct called Task that represents a task with a description and a Boolean flag indicating whether it is completed.

It also defines a TaskViewModel class that exposes an array of tasks and methods for loading and saving tasks.

Lastly, it defines a View called TaskView that displays a list of tasks and a button to add a new task.

The @ObservedObject property wrapper is used to enable the view to observe changes to the viewModel object.

The @Published property wrapper is used to make the tasks array observable so that changes to it can be published to the view. The onAppear modifier is used to load the tasks when the view appears.


Repository


One common way to implement MVVM is by using the Repository pattern to load data from a data source, such as a database or web service.

In this example, it is not necessary to add a Repository, but it could be a good idea for more complex apps.

The Repository pattern abstracts the data access layer of an application and decouples it from the rest of the app. By using a Repository, the ViewModel can interact with it to load and save data, instead of interacting directly with the data source.

Using a Repository can be especially helpful in a complex app with multiple data sources or complex data access requirements. Managing the data access layer directly in the ViewModel can be difficult in such cases, as the ViewModel may have to handle many different data sources and data access operations.

By using a Repository, you can move all of the data access logic out of the ViewModel, which can simplify the ViewModel and make it easier to manage.



import SwiftUI
import Combine
 
/// Define the model for a Task
struct Task: Identifiable {
    let id = UUID()
    let description: String
    let isCompleted: Bool
}
  
/// Define a view model that exposes tasks from the model for the view to consume
class TaskViewModel: ObservableObject {
    /// Create instance of TaskRepository to interact with data source
    private let taskRepository = TaskRepository()
 
    /// Published property wrapper allows other objects to subscribe to changes in tasks array
    @Published var tasks: [Task] = []
    private var cancellables: Set<AnyCancellable> = []
 
    init() {
        /// Subscribe to changes in tasks and assign them to tasks in TaskViewModel
        taskRepository.$tasks.assign(to: \.tasks, on: self)
            .store(in: &cancellables)
    }
 
    /// Function to load tasks from data source
    func loadData() {
        taskRepository.loadData()
    }
 
    /// Function to save task to data source
    func saveTask(task: Task) {
        taskRepository.saveTask(task: task)
    }
}
 
/// The Repository is responsible for loading the data from a data source
class TaskRepository {
    /// Published property wrapper allows other objects to subscribe to changes in tasks array
    @Published var tasks: [Task] = []
 
    /// Function to load tasks from data source
    func loadData() {
        let dailyTasks = [
            Task(description: "Connect OpenAi API", isCompleted: true),
            Task(description: "Weekly call with mobile team", isCompleted: false),
            Task(description: "Finish MVVM tutorial", isCompleted: false)
        ]
 
        tasks = dailyTasks
    }
 
    /// Function to save task to data source
    func saveTask(task: Task) {
        tasks.append(task)
    }
}

/// The TaskView is the main View of the app
struct TaskView: View {
    /// Create instance of TaskViewModel to interact with data
    @ObservedObject var viewModel = TaskViewModel()
 
    var body: some View {
        List(viewModel.tasks) { task in
            VStack(alignment:.leading){
                Text(task.description)
                    .font(.body)
                Text(task.isCompleted ? "Completed" : "To Do")
                    .font(.caption)
            }
        }.onAppear {
            /// Load data when view appears
            self.viewModel.loadData()
        }
 
        Button("Add New Task +", action: {
            /// Save new task when button is pressed
            viewModel.saveTask(task: Task(description: "New daily task",
                                           isCompleted: false))
        })
    }
}


In this example, the TaskRepository class is responsible for loading data from a data source, storing and managing task data, and providing methods for loading and saving tasks. The TaskViewModel exposes the data from the Task model in a way that is easy for the TaskView to consume.

Using a Repository like this is a good fit for the MVVM pattern because it separates data and logic in the app. All data access operations are handled by the Repository, instead of being scattered throughout the ViewModel.


Data Manager


Initializing the Repository in each ViewModel may not be the best approach. To avoid this, you can create a singleton DataManager class that initializes the repositories and provides a reference to them for the ViewModel.

Here is an example of how you can modify your code to use a DataManager singleton



import SwiftUI
import Combine

/// Define the model for a Task
struct Task: Identifiable {
    let id = UUID()
    let description: String
    let isCompleted: Bool
}

/// Define a view model that exposes tasks from the model for the view to consume
class TaskViewModel: ObservableObject {
    /// Use the task repository from the data manager to get tasks
    private let taskRepository = DataManager.shared.taskRepository
 
    /// Published property wrapper allows other objects to subscribe to changes in tasks array
    @Published var tasks: [Task] = []
    private var cancellables: Set<AnyCancellable> = []
 
    init() {
        /// Subscribe to changes in tasks and assign them to tasks in TaskViewModel
        taskRepository.$tasks.assign(to: \.tasks, on: self)
            .store(in: &cancellables)
    }
 
    /// Function to load tasks from data source
    func loadData() {
        taskRepository.loadData()
    }
 
    /// Function to save task to data source
    func saveTask(task: Task) {
        taskRepository.saveTask(task: task)
    }
}

/// The Repository is responsible for loading the data from a data source
class TaskRepository {
    /// Published property wrapper allows other objects to subscribe to changes in tasks array
    @Published var tasks: [Task] = []
 
    /// Function to load tasks from data source
    func loadData() {
        let dailyTasks = [
            Task(description: "Connect OpenAi API", isCompleted: true),
            Task(description: "Weekly call with mobile team", isCompleted: false),
            Task(description: "Finish MVVM tutorial", isCompleted: false)
        ]
 
        tasks = dailyTasks
    }
 
    /// Function to save task to data source
    func saveTask(task: Task) {
        tasks.append(task)
    }
}

/// Define a singleton data manager that provides a reference to the task repository
class DataManager {
    static let shared = DataManager()
    
    let taskRepository = TaskRepository()
}

/// The TaskView is the main View of the app
struct TaskView: View {
    /// Create instance of TaskViewModel to interact with data
    @ObservedObject var viewModel = TaskViewModel()
 
    var body: some View {
        List(viewModel.tasks) { task in
            VStack(alignment:.leading){
                Text(task.description)
                    .font(.body)
                Text(task.isCompleted ? "Completed" : "To Do")
                    .font(.caption)
            }
        }.onAppear {
            /// Load data when view appears
            self.viewModel.loadData()
        }
 
        Button("Add New Task +", action: {
            /// Save new task when button is pressed
            viewModel.saveTask(task: Task(description: "New daily task",
                                           isCompleted: false))
        })
    }
}


In this updated example, the DataManager is a singleton that initializes the TaskRepository and provides a reference to it for the TaskViewModel to use. This ensures that only a single instance of the TaskRepository is created, and it can be shared among multiple ViewModelobjects.


Managing multiple repositories with DataManager


Overall, using a DataManager as a singleton that initializes Repositories can provide several benefits. This approach ensures that there is only one instance of the DataManager, guarantees that all parts of the application are using the same Repositories, and makes it easier to manage dependencies between different parts of the application.

Here is an example of a DataManager class that can be used to manage the loading of data from multiple sources:


import SwiftUI
import Combine

/// Define the Task and User models
struct Task: Identifiable {
    let id = UUID()
    let description: String
    let isCompleted: Bool
}

struct User: Identifiable {
    let id = UUID()
    let name: String?
    let email: String?
}

/// Define a view model that exposes tasks from the model for the view to consume
class TaskViewModel: ObservableObject {
    /// Initialize repositories
    private let taskRepository = DataManager.shared.taskRepository
    private let userRepository = DataManager.shared.userRepository
    
    /// Published properties wrapper allows other objects to subscribe to changes
    @Published var tasks: [Task] = []
    @Published var user: User?
    private var cancellables: Set<AnyCancellable> = []
    
    init() {
        /// Subscribe to changes in tasks  and user data
        taskRepository.$tasks.assign(to: \.tasks, on: self)
            .store(in: &cancellables)
        userRepository.$user.assign(to: \.user, on: self)
            .store(in: &cancellables)
    }
    
    /// Function to load tasks from data source
    func loadData() {
        DataManager.shared.loadData()
    }
    
    /// Function to save task to data source
    func saveTask(task: Task) {
        taskRepository.saveTask(task: task)
    }
    
    /// Returns a greeting with the user's name (if available)
    func headerText() -> String {
        guard let name = user?.name else { return "Hi, here is your tasks:"}
        return "Hi, \(name) here is your tasks:"
    }
}

/// The Repository is responsible for loading the data from a data source
class TaskRepository {
    /// Published property wrapper allows other objects to subscribe to changes in tasks array
    @Published var tasks: [Task] = []
 
    /// Function to load tasks from data source
    func loadData() {
        let dailyTasks = [
            Task(description: "Connect OpenAi API", isCompleted: true),
            Task(description: "Weekly call with mobile team", isCompleted: false),
            Task(description: "Finish MVVM tutorial", isCompleted: false)
        ]
 
        tasks = dailyTasks
    }
 
    /// Function to save task to data source
    func saveTask(task: Task) {
        tasks.append(task)
    }
}

/// The UserRepository is responsible for loading user data
class UserRepository {
    @Published var user: User?
    
    /// Load the user data from the data source
    func loadData() {
        user = User(name: "Nazar", email: "nazar@tapforce.com")
    }
}

/// Define a singleton data manager that provides a reference to the task repository
class DataManager {
    static let shared = DataManager()
    
    let taskRepository = TaskRepository()
    let userRepository = UserRepository()
    
    /// Load data from the repositories
    func loadData() {
        taskRepository.loadData()
        userRepository.loadData()
    }
}

/// The TaskView is the main View of the app
struct TaskView: View {
    /// Create a TaskViewModel to manage data for the View
    @ObservedObject var viewModel = TaskViewModel()
    
    var body: some View {
        /// Present user welcome message
        Text(viewModel.headerText())
        
        /// Present tasks in a list with their descriptions and completion status
        List(viewModel.tasks) { task in
            VStack(alignment:.leading){
                Text(task.description)
                    .font(.body)
                Text(task.isCompleted ? "Completed" : "To Do")
                    .font(.caption)
            }
        }
        /// Load tasks when the view appears
        .onAppear {
            self.viewModel.loadData()
        }
        
        /// Allow the user to add a new task by clicking a button
        Button("Add New Task +", action: {
            viewModel.saveTask(task: Task(description: "New daily task",
                                          isCompleted: false))
        })
    }
}


This example demonstrates how DataManager can be used to manage multiple repositories and data sources:

When you add new data sources and repositories, you can simply add them to the DataManager and provide a reference to them for the ViewModel to use.

This approach is especially useful when you have multiple ViewModel objects that need to access the same data sources. By using the DataManager, you can avoid creating multiple instances of repositories and reduce the memory overhead of your application.

Welcome to our tutorial on the MVVM design pattern in SwiftUI! In this tutorial, we will explain the important concepts behind MVVM, including its components and how they work together. We will also give you advice and methods for using MVVM in your own projects.

After reading this tutorial, you will have a good understanding of MVVM and how it can assist you in writing simpler, more expandable code for your iOS apps.


Introduction to MVVM and its key concepts


MVVM (Model-View-ViewModel) is a design pattern used to organize the code for a user interface. It is an improved version of the MVC (Model-View-Controller) pattern and helps to better separate the presentation layer of an app from its underlying business logic. This makes it easier to manage the complexity of modern apps and allows for better code reuse and testability.

In summary, MVVM simplifies the process of writing clean, maintainable code for your iOS app.


The components of MVVM: Model, View, ViewModel



The Model represents the underlying data and business logic of the app. In the context of SwiftUI, it refers to the data that the app is working with, such as user and app data.

The View represents the user interface of the app. It displays the app's content and handles user interactions. In MVVM, the View is declarative and reactive, meaning that it automatically updates whenever the data in the ViewModel changes.

The ViewModel is like a middleman between the Model and View. It shows the data and actions that the View needs, and it manages the communication between the Model and View. The ViewModel doesn't know about the View, and it only works with the data and actions that the View requires to show.


Basic implementing MVVM in SwiftUI:


Let's first start with a basic example. This will help us better understand the concepts behind MVVM and how it can be applied to your own projects:



import SwiftUI
import Combine

/// Define the model for a Task
struct Task: Identifiable {
    let id = UUID()
    let description: String
    let isCompleted: Bool
}

/// Define a view model that exposes tasks from the model for the view to consume
class TaskViewModel: ObservableObject {
    /// The list of tasks
    @Published var tasks: [Task] = []
    
    /// Function to load tasks from data source
    func loadData() {
        let dailyTasks = [
            Task(description: "Connect OpenAi API", isCompleted: true),
            Task(description: "Weekly call with mobile team", isCompleted: false),
            Task(description: "Finish MVVM tutorial", isCompleted: false)
        ]
        tasks = dailyTasks
    }
    
    /// Function to save task to data source
    func saveTask(task: Task) {
        tasks.append(task)
    }
}

/// The TaskView is the main View of the app
struct TaskView: View {
    /// Create instance of TaskViewModel to interact with data
    @ObservedObject var viewModel = TaskViewModel()
 
    var body: some View {
        List(viewModel.tasks) { task in
            VStack(alignment:.leading){
                Text(task.description)
                    .font(.body)
                Text(task.isCompleted ? "Completed" : "To Do")
                    .font(.caption)
            }
        }.onAppear {
            /// Load data when view appears
            self.viewModel.loadData()
        }
 
        Button("Add New Task +", action: {
            /// Save new task when button is pressed
            viewModel.saveTask(task: Task(description: "New daily task",
                                           isCompleted: false))
        })
    }
}


Example defines a struct called Task that represents a task with a description and a Boolean flag indicating whether it is completed.

It also defines a TaskViewModel class that exposes an array of tasks and methods for loading and saving tasks.

Lastly, it defines a View called TaskView that displays a list of tasks and a button to add a new task.

The @ObservedObject property wrapper is used to enable the view to observe changes to the viewModel object.

The @Published property wrapper is used to make the tasks array observable so that changes to it can be published to the view. The onAppear modifier is used to load the tasks when the view appears.


Repository


One common way to implement MVVM is by using the Repository pattern to load data from a data source, such as a database or web service.

In this example, it is not necessary to add a Repository, but it could be a good idea for more complex apps.

The Repository pattern abstracts the data access layer of an application and decouples it from the rest of the app. By using a Repository, the ViewModel can interact with it to load and save data, instead of interacting directly with the data source.

Using a Repository can be especially helpful in a complex app with multiple data sources or complex data access requirements. Managing the data access layer directly in the ViewModel can be difficult in such cases, as the ViewModel may have to handle many different data sources and data access operations.

By using a Repository, you can move all of the data access logic out of the ViewModel, which can simplify the ViewModel and make it easier to manage.



import SwiftUI
import Combine
 
/// Define the model for a Task
struct Task: Identifiable {
    let id = UUID()
    let description: String
    let isCompleted: Bool
}
  
/// Define a view model that exposes tasks from the model for the view to consume
class TaskViewModel: ObservableObject {
    /// Create instance of TaskRepository to interact with data source
    private let taskRepository = TaskRepository()
 
    /// Published property wrapper allows other objects to subscribe to changes in tasks array
    @Published var tasks: [Task] = []
    private var cancellables: Set<AnyCancellable> = []
 
    init() {
        /// Subscribe to changes in tasks and assign them to tasks in TaskViewModel
        taskRepository.$tasks.assign(to: \.tasks, on: self)
            .store(in: &cancellables)
    }
 
    /// Function to load tasks from data source
    func loadData() {
        taskRepository.loadData()
    }
 
    /// Function to save task to data source
    func saveTask(task: Task) {
        taskRepository.saveTask(task: task)
    }
}
 
/// The Repository is responsible for loading the data from a data source
class TaskRepository {
    /// Published property wrapper allows other objects to subscribe to changes in tasks array
    @Published var tasks: [Task] = []
 
    /// Function to load tasks from data source
    func loadData() {
        let dailyTasks = [
            Task(description: "Connect OpenAi API", isCompleted: true),
            Task(description: "Weekly call with mobile team", isCompleted: false),
            Task(description: "Finish MVVM tutorial", isCompleted: false)
        ]
 
        tasks = dailyTasks
    }
 
    /// Function to save task to data source
    func saveTask(task: Task) {
        tasks.append(task)
    }
}

/// The TaskView is the main View of the app
struct TaskView: View {
    /// Create instance of TaskViewModel to interact with data
    @ObservedObject var viewModel = TaskViewModel()
 
    var body: some View {
        List(viewModel.tasks) { task in
            VStack(alignment:.leading){
                Text(task.description)
                    .font(.body)
                Text(task.isCompleted ? "Completed" : "To Do")
                    .font(.caption)
            }
        }.onAppear {
            /// Load data when view appears
            self.viewModel.loadData()
        }
 
        Button("Add New Task +", action: {
            /// Save new task when button is pressed
            viewModel.saveTask(task: Task(description: "New daily task",
                                           isCompleted: false))
        })
    }
}


In this example, the TaskRepository class is responsible for loading data from a data source, storing and managing task data, and providing methods for loading and saving tasks. The TaskViewModel exposes the data from the Task model in a way that is easy for the TaskView to consume.

Using a Repository like this is a good fit for the MVVM pattern because it separates data and logic in the app. All data access operations are handled by the Repository, instead of being scattered throughout the ViewModel.


Data Manager


Initializing the Repository in each ViewModel may not be the best approach. To avoid this, you can create a singleton DataManager class that initializes the repositories and provides a reference to them for the ViewModel.

Here is an example of how you can modify your code to use a DataManager singleton



import SwiftUI
import Combine

/// Define the model for a Task
struct Task: Identifiable {
    let id = UUID()
    let description: String
    let isCompleted: Bool
}

/// Define a view model that exposes tasks from the model for the view to consume
class TaskViewModel: ObservableObject {
    /// Use the task repository from the data manager to get tasks
    private let taskRepository = DataManager.shared.taskRepository
 
    /// Published property wrapper allows other objects to subscribe to changes in tasks array
    @Published var tasks: [Task] = []
    private var cancellables: Set<AnyCancellable> = []
 
    init() {
        /// Subscribe to changes in tasks and assign them to tasks in TaskViewModel
        taskRepository.$tasks.assign(to: \.tasks, on: self)
            .store(in: &cancellables)
    }
 
    /// Function to load tasks from data source
    func loadData() {
        taskRepository.loadData()
    }
 
    /// Function to save task to data source
    func saveTask(task: Task) {
        taskRepository.saveTask(task: task)
    }
}

/// The Repository is responsible for loading the data from a data source
class TaskRepository {
    /// Published property wrapper allows other objects to subscribe to changes in tasks array
    @Published var tasks: [Task] = []
 
    /// Function to load tasks from data source
    func loadData() {
        let dailyTasks = [
            Task(description: "Connect OpenAi API", isCompleted: true),
            Task(description: "Weekly call with mobile team", isCompleted: false),
            Task(description: "Finish MVVM tutorial", isCompleted: false)
        ]
 
        tasks = dailyTasks
    }
 
    /// Function to save task to data source
    func saveTask(task: Task) {
        tasks.append(task)
    }
}

/// Define a singleton data manager that provides a reference to the task repository
class DataManager {
    static let shared = DataManager()
    
    let taskRepository = TaskRepository()
}

/// The TaskView is the main View of the app
struct TaskView: View {
    /// Create instance of TaskViewModel to interact with data
    @ObservedObject var viewModel = TaskViewModel()
 
    var body: some View {
        List(viewModel.tasks) { task in
            VStack(alignment:.leading){
                Text(task.description)
                    .font(.body)
                Text(task.isCompleted ? "Completed" : "To Do")
                    .font(.caption)
            }
        }.onAppear {
            /// Load data when view appears
            self.viewModel.loadData()
        }
 
        Button("Add New Task +", action: {
            /// Save new task when button is pressed
            viewModel.saveTask(task: Task(description: "New daily task",
                                           isCompleted: false))
        })
    }
}


In this updated example, the DataManager is a singleton that initializes the TaskRepository and provides a reference to it for the TaskViewModel to use. This ensures that only a single instance of the TaskRepository is created, and it can be shared among multiple ViewModelobjects.


Managing multiple repositories with DataManager


Overall, using a DataManager as a singleton that initializes Repositories can provide several benefits. This approach ensures that there is only one instance of the DataManager, guarantees that all parts of the application are using the same Repositories, and makes it easier to manage dependencies between different parts of the application.

Here is an example of a DataManager class that can be used to manage the loading of data from multiple sources:


import SwiftUI
import Combine

/// Define the Task and User models
struct Task: Identifiable {
    let id = UUID()
    let description: String
    let isCompleted: Bool
}

struct User: Identifiable {
    let id = UUID()
    let name: String?
    let email: String?
}

/// Define a view model that exposes tasks from the model for the view to consume
class TaskViewModel: ObservableObject {
    /// Initialize repositories
    private let taskRepository = DataManager.shared.taskRepository
    private let userRepository = DataManager.shared.userRepository
    
    /// Published properties wrapper allows other objects to subscribe to changes
    @Published var tasks: [Task] = []
    @Published var user: User?
    private var cancellables: Set<AnyCancellable> = []
    
    init() {
        /// Subscribe to changes in tasks  and user data
        taskRepository.$tasks.assign(to: \.tasks, on: self)
            .store(in: &cancellables)
        userRepository.$user.assign(to: \.user, on: self)
            .store(in: &cancellables)
    }
    
    /// Function to load tasks from data source
    func loadData() {
        DataManager.shared.loadData()
    }
    
    /// Function to save task to data source
    func saveTask(task: Task) {
        taskRepository.saveTask(task: task)
    }
    
    /// Returns a greeting with the user's name (if available)
    func headerText() -> String {
        guard let name = user?.name else { return "Hi, here is your tasks:"}
        return "Hi, \(name) here is your tasks:"
    }
}

/// The Repository is responsible for loading the data from a data source
class TaskRepository {
    /// Published property wrapper allows other objects to subscribe to changes in tasks array
    @Published var tasks: [Task] = []
 
    /// Function to load tasks from data source
    func loadData() {
        let dailyTasks = [
            Task(description: "Connect OpenAi API", isCompleted: true),
            Task(description: "Weekly call with mobile team", isCompleted: false),
            Task(description: "Finish MVVM tutorial", isCompleted: false)
        ]
 
        tasks = dailyTasks
    }
 
    /// Function to save task to data source
    func saveTask(task: Task) {
        tasks.append(task)
    }
}

/// The UserRepository is responsible for loading user data
class UserRepository {
    @Published var user: User?
    
    /// Load the user data from the data source
    func loadData() {
        user = User(name: "Nazar", email: "nazar@tapforce.com")
    }
}

/// Define a singleton data manager that provides a reference to the task repository
class DataManager {
    static let shared = DataManager()
    
    let taskRepository = TaskRepository()
    let userRepository = UserRepository()
    
    /// Load data from the repositories
    func loadData() {
        taskRepository.loadData()
        userRepository.loadData()
    }
}

/// The TaskView is the main View of the app
struct TaskView: View {
    /// Create a TaskViewModel to manage data for the View
    @ObservedObject var viewModel = TaskViewModel()
    
    var body: some View {
        /// Present user welcome message
        Text(viewModel.headerText())
        
        /// Present tasks in a list with their descriptions and completion status
        List(viewModel.tasks) { task in
            VStack(alignment:.leading){
                Text(task.description)
                    .font(.body)
                Text(task.isCompleted ? "Completed" : "To Do")
                    .font(.caption)
            }
        }
        /// Load tasks when the view appears
        .onAppear {
            self.viewModel.loadData()
        }
        
        /// Allow the user to add a new task by clicking a button
        Button("Add New Task +", action: {
            viewModel.saveTask(task: Task(description: "New daily task",
                                          isCompleted: false))
        })
    }
}


This example demonstrates how DataManager can be used to manage multiple repositories and data sources:

When you add new data sources and repositories, you can simply add them to the DataManager and provide a reference to them for the ViewModel to use.

This approach is especially useful when you have multiple ViewModel objects that need to access the same data sources. By using the DataManager, you can avoid creating multiple instances of repositories and reduce the memory overhead of your application.