MVVM for?iOS
Hello Everyone,
Here is another article by me that explains my understanding of the different iOS Architecture.
Every day we as a developer we write our code which is related to UI or Business Logic and we always want it to be very clear and self-explanatory.
How we usually make our code very clear by formatting our code with proper indents and most importantly by giving meaningful and clear variable and methods name. We can use the basic OOPS concept like inheritance, polymorphism, abstraction, etc which helps to reduce the amount of code and we can also refactor functions to inner functions as well which helps us in debugging.
So the question arises here is why the developer gives importance to the Architecture of the code.
Refactoring of code is good but ultimately that particular class or controller still has the same amount of work to do. However, the code looks cleaner with formatting and refactoring which is good.
Now, As we all know developers are the laziest person especially me, and we do not like to write much code so what we should do here is reuse the same logic or UI behaviour at different places of the project or even further at different projects. So It should be like simple plug it and play it rule apply and to make it like that we have to divide our code into different classes so that each class will be responsible to work specific tasks only and that way we can make it reusable.
The Architectures
Now another question arises here is that What are the types of architecture and which one we should go with based on requirements or the size of the project.
Selecting the Architectures to best fit your needs.
I have read many blogs that say that we should select the architecture based on the size of the team and also the complexity of the project.
My opinion is a bit different here, I believe the developer should select the architecture in which they are comfortable the most. As long as the developer is comfortable with the code and they can easily debug or test their implementation they are good to go and of course at the end when you launch the product, no one is going to see how you code they will only judge you by the performance of the application. The only key rule is you should stick to architecture you followed from the beginning and make your code consistent.
Type Of Architecture
Today I would like to talk about the 2 most common architecture and how they have differed.
- MVC ( Model View Controller )
- MVVM ( Model View View Model )
MVC
Let’s talk about the first one, The MVC Architecture which is proposed by Apple. MVC is one of the easiest architectural to be work with as it has only 3 players in action.
The View, which is responsible for the user experience of the app and also detects the user action on the screen.
The Model, which contains all your data which is being displayed by the View. Usually, simple classes which contain variable and helper methods.
The Controller, Which communicates to both view and model and response to user interactions.
As I said this is very easy to use architecture and which is usually followed by the freshers mostly.
The only reason I will not go with this is that when your 5 screens project becomes 50 screen projects and each screen’s all the functionality, the Business Logic and UI part will remain in the same controller which is going to be the painful thing when you start debugging.
Also, business logic and UI logic are attached to the controller so there will be the least chances that you reuse the same logic in other classes.
Of course, you can copy and paste it in different places… lol
MVVM
The Most Trending and Fabulous MVVM.
so it also has the same 3 workers in it so let’s see what they are and how they are different from MVC.
As we see from the above diagram, We have following workers in the architecture.
- Model
- View
- ViewModel
The reason why MVVM becomes more famous among the developers is that the most expensive piece of code of your project which the core business logic will be in your ViewModel and Model Classes so your view controller does not know what the logic is being applied and model classes will simply hold the data.
ViewModel will only provide instruction to controller what it needs to do next or when specific events occurred.
Like in my example, when users log in with Facebook or Google or email the viewController will only get 2 notification which User failed to login or login successfully and that is also via Protocol and Delegate.
import UIKit import FBSDKLoginKit import FBSDKCoreKit typealias ValidationStatus = (status:Bool,errorMessage:String) protocol LoginViewModelProtocol:class { func userLoggedInSuccessfully() -> Void func failedToLoggedIn(withErrorMessage errorMessage:String) -> Void } class LoginViewModel: NSObject { weak private var delegate:LoginViewModelProtocol? private var fbManager:MSFacebookHelper? private var googleManager:MSGoogleHelper? private lazy var localizeStrings = ValidationMessages() init(withDelegate delegate:LoginViewModelProtocol) { super.init() self.delegate = delegate } @discardableResult func setupFacebook() -> LoginButtonDelegate?{ fbManager = MSFacebookHelper.init(withDelegate: self) return fbManager } func setupGoogle(withController controller:UIViewController) -> Void{ self.googleManager = MSGoogleHelper.init(withPresentingController: controller, andDelegate: self) } func validateCredentials(withEmail email:String?,andPassword password:String?) -> Void { // Validate Email var validationResult = self.validate(email: email) guard validationResult.status == true else { self.delegate?.failedToLoggedIn(withErrorMessage: validationResult.errorMessage) return } // Validate Password validationResult = self.validate(password: password) guard validationResult.status == true else { self.delegate?.failedToLoggedIn(withErrorMessage: validationResult.errorMessage) return } // Check for Internet Connection guard MSServiceManager.shared.isInternetAvailable() else { self.delegate?.failedToLoggedIn(withErrorMessage: ValidationMessages().internetConnectionError) return } // Call Login API // Implement your login api call here self.delegate?.failedToLoggedIn(withErrorMessage: "You forgot to implement your LOGIN API call in LoginViewModel Class\nOr you can Try to login with Google or Facebook") } } // Validate your inputs extension LoginViewModel{ func validate(email:String?) -> ValidationStatus { guard let emailValue = email,emailValue.isEmpty == false else { return (false,localizeStrings.enterEmail) } if emailValue.isValidEmail() == false{ return (false,localizeStrings.enterValidEmail) } return (true,"") } func validate(password:String?) -> ValidationStatus { guard let passwordValue = password, passwordValue.isEmpty == false else { return (false,localizeStrings.enterPassword) } return (true,"") } } // Handle Google Login Events extension LoginViewModel:MSGoogleHelperEventsProtocol{ func googleUserLoggedInSuccessfully(withInformation authInfo: MSFirebaseManager.AuthenticationInfo) { MSUser.current.updateDetails(WithContent: authInfo) self.delegate?.userLoggedInSuccessfully() } func failedToValidateGoogleUser(withError error: Error?) { self.googleManager?.logout() self.delegate?.failedToLoggedIn(withErrorMessage: error?.localizedDescription ?? GlobalMessages().somethingWentWrongWithServer) } func googleLogoutSuccessfully() { } } // Handle Facebook Login Events extension LoginViewModel:MSFacebookHelpersEventsProtocol{ func fbUserLoggedInSuccessfully(withInformation authInfo: MSFirebaseManager.AuthenticationInfo) { MSUser.current.updateDetails(WithContent: authInfo) self.delegate?.userLoggedInSuccessfully() } func failedToValidateFBUser(withError error: Error?) { self.fbManager?.logout() self.delegate?.failedToLoggedIn(withErrorMessage: error?.localizedDescription ?? GlobalMessages().somethingWentWrongWithServer) } func facebookLogoutSuccessfully() { } }
As we can see in the above LoginViewModel class, it is responsible for validating the inputs like email and password validation or handle events of Google or Facebook login.
When a user is validated by either Google or Facebook or via Email and Password it will only let the controller know that it’s a valid user and proceed ahead.
This is specifically for ViewController but what about the TableView? How can we add abstraction between our UITableView by following MVVM and where should we write our display logic for UITableViewCell.
Well, here is another example with UITableView as well.
Here we have our model class which will simply hold the data which we will get from our API
struct Country:Codable { var name:String var capital:String init(withName name:String,andCapital capital:String) { self.name = name self.capital = capital } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.name, forKey: .name) try container.encode(self.capital, forKey: .capital) } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.name = try container.decode(String.self, forKey: .name) self.capital = try container.decode(String.self, forKey: .capital) } enum CodingKeys:String,CodingKey { case name case capital } }
Here is ViewModel class which will have business logic and private reference to our model class. As you can see we are using methods to return value instead of making the model publicly available and in methods ViewModel, we can apply our business logic.
class CountryViewModel: NSObject { private var countryInfo:Country init(withCountry country:Country){ self.countryInfo = country super.init() } // Add your display logic here func getName() -> String{ return self.countryInfo.name } func getCapital() -> String{ return self.countryInfo.capital } func getDescription() -> String { return "\(self.getName()), \(self.getCapital())" } }
And here is the ViewModel for CountryListViewController which will actually return the array of ViewModel classes.
import UIKit protocol CountryListViewModelProtocol:class { func didReceive(countries aryCountries:[CountryViewModel]) -> Void func failedToGetCountries(withError error:Error) -> Void } class CountryListViewModel: NSObject { weak private var delegate:CountryListViewModelProtocol? init(withDelegate delegate:CountryListViewModelProtocol){ self.delegate = delegate } func fetchCountries() -> Void{ MSServiceManager.shared.getCountryList {[weak self] (aryCountry, error) in if let error = error{ self?.delegate?.failedToGetCountries(withError: error) }else{ var aryCountryViewModel:[CountryViewModel] = [] for country in aryCountry{ aryCountryViewModel.append(CountryViewModel.init(withCountry: country)) } self?.delegate?.didReceive(countries: aryCountryViewModel) } } } }
Have a look at the GitHub repository to see it in action.
Another good factor about MVVM is since the logic remains in separate files you can verify your business logic against different inputs.
There are 3 test cases in the code. One will validate the working of CountryViewModel based on the Country model. Another will validate the working of API and the last one will check the response time of the API as it’s also important.
import XCTest @testable import MVVM_Demo class MVVM_DemoTests: XCTestCase { func testCountryViewModel() { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. let country = Country.init(withName: "Test", andCapital: "Hello") let countryViewModel = CountryViewModel.init(withCountry: country) // Here we make sure that view model returns same value as we had in our model. XCTAssertTrue(country.name == countryViewModel.getName()) // Here we make sure the output of getDescription methods is same as in expected format XCTAssertTrue(countryViewModel.getDescription() == "\(country.name), \(country.capital)") } func testGetCountryListAPICall() -> Void{ // This test will make sure our API is working fine and correct country list will be displayed. let promise = expectation(description: "Country List Request") MSServiceManager.shared.getCountryList { (aryCountry, error) in if aryCountry.count > 0{ XCTAssertNil(error) promise.fulfill() }else{ XCTAssertTrue(error != nil) } } wait(for: [promise], timeout: 5.0) } func testPerformanceExample() { // This is an example of a performance test case. measure { // Put the code you want to measure the time of here. self.testGetCountryListAPICall() } } }
I hope you guys like my blog and it clears up most of the concept regarding the MVVM and how it actually works in real-world projects.
Let me know if you have any queries or have any suggestions for my GitHub starter project.
Thanks for reading and Happy Coding.
iOS Application Developer
2 年there is a no need to import UIKit in viewModel ?? .
This is MVVM-C + Rxswift demo?https://github.com/anshul18895/MVVMC-Demo
Principal Engineer at Volansys Technologies Pvt Ltd
5 年Awesome