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.