Home About Contact

 

SwiftUI MVVM with practical examples

5 min read

MVVM – the Model View ViewModel architecture, is a better design approach for application to ensure there is no tight coupling between your application UI, business logic and the data layer. So, any future changes of any of these components should not affect one another.

Whether you’re working in a team or developing single-handedly, using MVVM design approach in your mobile app development should allow for cleaner code, better reusability, maintainability and flexibility of future change and growth of complexity of your app.

The diagram below should be just nice to describe the relationships between a Model, a ViewModel and a View.

SwiftUI MVVM

  • Model – it represents the data from the datasource, which can be from remote URL or local e.g. REST API in Json or XML format from remote URL or local data stored in JSON files in local Bundle, or local data stored in CoreData database etc.

    A Model should just be kept as simple as to reflect directly what the data structure of the datasource is.

  • ViewModel – A ViewModel is the bridge between the View and the Model. A ViewModel is used by View for displaying different View states but it should not be aware what View is using it.

    A ViewModel should contain some business logic for preparing the data from the Model in the forms required by the View; so there is no tight coupling between the View and the Model. A ViewModel should also update a Model when needed.
  • View – It’s the UI i.e. made up of a combination of List, ScrollView, Text, Image and many others in SwiftUI. In an MVVM approach, View is dependent on ViewModel to provide it with different View states. And it also informs the ViewModel about the user interactions.

Let’s look at a simple practical example in SwiftUI. For simplicity, our datasource is just a Json file – Employees.json stored in local Bundle. The Json data structure is as simple as below:

[
    {
        "name" : "Johnson T. Campbell",
        "dateJoined" : "2012-10-01"
    },
    {
        "name" : "Cierra Vega",
        "dateJoined" : "2019-02-01"
    }
    ...
]

The Model

For simplicity, the above is just a list of employees which each only contains the name of the employee and the date of the employee joined the company (in the format of yyyy-MM-dd). So it can just be mapped to the following data Model.

struct Employee : Decodable {
    var name : String
    var dateJoined : Date
}

The ViewModel

The ViewModel is responsible of loading the data and it’s observable by the View, whereas a change of its data will directly reflect to the View states.

It’s also a wrapper of the Model and prepares the data in the required format for the View to display.

The code for our ViewModel :

class EmployeeViewModel : ObservableObject, Identifiable {
    @Published private var allEmployeeViewModels : [EmployeeViewModel] = []
    
    private var employee : Employee?
   
    init(employee : Employee? = nil ){
        self.employee = employee
    }
    
    var id = UUID()
    
    var name : String {
        employee?.name ?? "" // defaulted to blank string if nil
    }
    
    var dateJoined : String {    
        if let dj = employee?.dateJoined {        
            return DateFormatter().string(from : dj, dateFormat: "dd/MMM/yy")
        }    
        return ""
    }
    
    var numberOfYearsServed : Int {
        if let dj = employee?.dateJoined {
            let yearComp = Calendar.current.dateComponents([.year], from: dj, to: Date())
            if let year = yearComp.year{                
                return year
            }            
        }        
        return 0
    }

    var allEmployees : [EmployeeViewModel] {       
        self.allEmployeeViewModels.sorted {           
            $0.name < $1.name
        }
    }
}

ObservableObject and @Published property wrapper provided by Combine framework

In order for EmployeeViewModel to be observable for its changes by the views, it needs to conform to the ObservableObject protocol. And by using the property wrapper @Published allows it to automatically announce the changes of that property to the views.

In the above code, the variable allEmployeeViewModels, which is an array of EmployeeViewModel, has the @Published property wrapper, therefore any changes of this array will be directly reflected to the View that is using it.

Some explanation :

The EmployeeViewModel is also a wrapper of the model Employee. Its initializer takes an optional Employee object. It basically wraps the properties of the Employee model and transforms them into the required values or formats for the Views and with a few additional.

Such as the numberOfYearsServed, which encapsulates logic to calculate the number of years an employee has served in the company based on the dateJoined. Thanks to Calendar - the difference between the dateJoined and now in number of years, can just be done in one line of code.

The dateJoined is now a String of date with the format "dd/MMM/yy", which is converted by using an extension function of DateFormatter. You can refer it from our previous tutorial.

The allEmployees is basically a proxy property of the allEmployeeViewModels sorted according to the alphabetical order of the name property.

It conforms to the Identifiable in order for it to be used by a SwiftUI List and has a unique id by UUID().

The EmployeeViewModel is responsible for loading data - in our example here, it loads and decodes from a Json file stored in local bundle resource by using the extension function Bundle.main.decodeJson. You can refer to this extension function here.

And the date decoding strategy is supplied by another extension function used in our previous tutorial for decoding date format "yyyy-MM-dd".

The code for loading data:

extension EmployeeViewModel {   
    func loadEmployees () {
        // decoded into an array of Employee
        let employees = Bundle.main.decodeJson(
            [Employee].self, fileName: "Employees.json",
            dateDecodingStrategy:
            DateFormatter().jsonDateDecodingStrategy(dateFormat: "yyyy-MM-dd"))
        // clear all first
        self.allEmployeeViewModels.removeAll()
        // publish in the @Published allEmployeeViewModels 
        employees.forEach { employee in
            self.allEmployeeViewModels.append(EmployeeViewModel(employee: employee))
        }   
    }
}

The View layer

We basically have two views here - the main view and the row view. The main view is basically just a List with its data from the EmployeeViewModel's property "allEmployees" - which sorts the published property "allEmployeeViewModels" by the employee's name in alphabetical order.

EmployeeViewModel is an observed object with the property wrapper @ObservedObject so any change of its published properties should cause the views to update.

In the onAppear() of the List, we invoke the loadEmployees() of the EmployeeViewModel to load the data

The main view - Example2 :

struct Example2 : View {
    @ObservedObject var employeeViewModel = EmployeeViewModel()

    var body: some View {
        
        List (employeeViewModel.allEmployees, id:\.id) {
            employee in
            EmployeeRowView(employee: employee)
        }
        .onAppear{
            employeeViewModel.loadEmployees()
        }
    }
}

The row view which takes an EmployeeViewModel and displays the name, dateJoined and the numberOfYearsServed as follows:

struct EmployeeRowView : View {

    private var employee : EmployeeViewModel

    init(employee : EmployeeViewModel){
        self.employee = employee
    }
    
    var body: some View {

        VStack(alignment: .leading, spacing: 10) {
        
            Text(employee.name)
            .font(.headline)
            
            Text("Date Joined : \(employee.dateJoined), Years served : \(employee.numberOfYearsServed)")
            .font(.caption)
            
        }
        .frame(height: 50)
    }
}

The result is as follows:

MVVM - list of employees

Lets add some user interaction!

We want to show different list of employees for different range of number of years that an employee has served in the company.

So, when the user selects a particular range it'll show the list of employees filtered by the selected range of years, as follows:

So, let's do it!

We'll add another @Published property - filteredEmployeeViewModels in our EmployeeViewModel :

@Published private var filteredEmployeeViewModels :[EmployeeViewModel] = []

And we have a proxy property filteredEmployees, which basically sorts the filteredEmployeeViewModels by the number of years an employee has served in ascending order, as below:

var filteredEmployees : [EmployeeViewModel] {   
    self.filteredEmployeeViewModels.sorted {       
       $0.numberOfYearsServed < $1.numberOfYearsServed
    }
}

And a function yearsServed(_ Int: _ Int) which filters the allEmployeeViewModels by the provided year range into filteredEmployeeViewModels :

extension EmployeeViewModel {
    func yearsServed(_ start : Int = 0 , _ end : Int = 5){
        // load the data into allEmployeeViewModels 
        // first if it's empty
        if ( allEmployeeViewModels.count == 0 ){
            self.loadEmployees()
        }

        self.filteredEmployeeViewModels = self.allEmployeeViewModels.filter{   
            $0.numberOfYearsServed >= start && $0.numberOfYearsServed <= end
        }
    }
}

On the View layer, we need to prepare a horizontal ScrollView of Text views with different ranges of years for the user to tap to select, let's just call it ScrollViewOfYearRanges, as follows:

struct ScrollViewOfYearRanges : View {
    @State private var yearSelectedIndex = 0
   
    var employeeViewModel : EmployeeViewModel
    
    var body : some View {
        let yearRanges  = [ (0,5), (6,10), (11,15), (16,20), (21,25) ]

        ScrollView (.horizontal, showsIndicators: false ) {
           
            HStack {
           
                ForEach ( (0..<yearRanges.count)){
                    idx in
                    
                    let yr = yearRanges[idx]
            
                    Text("\(yr.0) - \(yr.1)")
                    .frame(width: 80, height: 40)
                    .foregroundColor(.white)
                    .background( (self.yearSelectedIndex == idx) ? Color.green : Color.gray )
                    .cornerRadius(20)
                    .onTapGesture {
                        self.employeeViewModel.yearsServed( yr.0, yr.1 )
                        self.yearSelectedIndex = idx
                    }
                    
                }
                    
            }
        }
        .frame(alignment: .leading)
        
    }
}

The below is how it's shown on the Xcode preview.

The UI action - the onTapGesture of the Text view will invoke the yearsServed(_ Int : _ Int) function of the EmployeeViewModel with the selected range of years.

Put them together in our main view - Example3. The ObservedObject employeeViewModel is shared with the child view ScrollViewOfYearRanges :

struct Example3 : View {
    @ObservedObject var employeeViewModel = EmployeeViewModel()
    
    var body: some View {
    
        VStack(alignment: .leading, spacing: 20) {
            
            ScrollViewOfYearRanges(employeeViewModel: employeeViewModel)
            .padding()
            
            List(employeeViewModel.filteredEmployees, id:\.id){
                employee in
                EmployeeRowView(employee: employee)
            }
            .onAppear{
                // call the yearsServed() with default range
                // of 0-5, which also loads the data if it's empty
                employeeViewModel.yearsServed()
            }
        }
    }
}

That's all for now in order not to make this blog post too lengthy!. The complete source code of a couple of examples in this tutorial can be found on GitHub - including an example of loading data from remote URL by using the extension function of URLSession. Happy Learning!

Spread the love
Posted on February 7, 2021 By Christopher Chee

4 thoughts on “SwiftUI MVVM with practical examples”

  1. zebrum says:

    SwiftUI’s structs List, HStack etc. that form its declarative syntax are not Views in the MVC/MVVM sense. Those are more anagulous to View Models. SwiftUI builds Views, UITableView etc. from these structs and diffs the structs to update these Views.

    1. Christopher Chee says:

      Hi,

      Thanks for the comment. Btw, hope you’d be more specific about it or you may have some confusion about MVVM.

      In MVVM, the SwiftUI views such as List, HStack etc are just the Views. The ViewModel is a bridge in between Model and the View, which transforms values into the proper formats from the Model for the Views to display.

      There should be NO view in a ViewModel, and you should not import SwiftUI in a ViewModel as a ViewModel does NOT depend on any Views and is not aware of what Views are using it.

      And a ViewModel should provide an abstraction to Model, whereas in the future when there is a change of our datasource will not affect the need to change on the View as it’s been handled by the ViewModel.

      Thanks for the comment anyway 😀

Please leave us your comments below, if you find any errors or mistakes with this post. Or you have better idea to suggest for better result etc.


Our FB Twitter Our IG Copyright © 2024